root / pykota / trunk / bin / cupspykota @ 3018

Revision 3018, 69.8 kB (checked in by jerome, 18 years ago)

Ensures that the billing code and username are encoded into the user's charset
after they have been overwritten through the overwrite_jobticket directive.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# CUPSPyKota accounting backend
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import os
29import fcntl
30import time
31import errno
32import tempfile
33import popen2
34import cStringIO
35import shlex
36import signal
37import md5
38import fnmatch
39import pwd
40import socket
41import smtplib
42from email.MIMEText import MIMEText
43from email.Header import Header
44import email.Utils
45
46from mx import DateTime
47
48from pykota.tool import PyKotaTool, PyKotaToolError, crashed
49from pykota.accounter import openAccounter
50# TODO : remove the three lines below and the code which handles
51# TODO : them in a future release.
52from pykota.ipp import IPPRequest as oldIPPRequest
53from pykota.ipp import IPPError as oldIPPError
54
55try :
56    from pkipplib import pkipplib
57except ImportError :       
58    haspkipplib = False
59else :   
60    haspkipplib = True
61   
62class FakeObject :       
63    """Fake object."""
64    def __init__(self, name) :
65        """Fake init."""
66        self.Name = name
67       
68class FakePrinter(FakeObject) :       
69    """Fake printer instance."""
70    pass
71   
72class FakeUser(FakeObject) :   
73    """Fake user instance."""
74    def __init__(self, name) :
75        """Fake init."""
76        self.Email = name
77        FakeObject.__init__(self, name)
78   
79class CUPSBackend(PyKotaTool) :
80    """Base class for tools with no database access."""
81    def __init__(self) :
82        """Initializes the CUPS backend wrapper."""
83        PyKotaTool.__init__(self)
84        signal.signal(signal.SIGTERM, signal.SIG_IGN)
85        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
86        self.MyName = "PyKota"
87        self.myname = "cupspykota"
88        self.pid = os.getpid()
89        self.DataFile = None
90        self.lockfilename = None
91        self.lockfile = None
92       
93    def deferredInit(self) :   
94        """Deferred initialization."""
95        PyKotaTool.deferredInit(self)
96        self.gotSigTerm = 0
97        self.disableSigInt()
98        self.installSigTermHandler()
99       
100    def sigtermHandler(self, signum, frame) :
101        """Sets an attribute whenever SIGTERM is received."""
102        self.gotSigTerm = 1
103        self.printInfo(_("SIGTERM received, job %s cancelled.") % self.JobId)
104        os.environ["PYKOTASTATUS"] = "CANCELLED"
105       
106    def deinstallSigTermHandler(self) :           
107        """Deinstalls the SIGTERM handler."""
108        self.logdebug("Deinstalling SIGTERM handler...")
109        signal.signal(signal.SIGTERM, signal.SIG_IGN)
110        self.logdebug("SIGTERM handler deinstalled.")
111       
112    def installSigTermHandler(self) :           
113        """Installs the SIGTERM handler."""
114        self.logdebug("Installing SIGTERM handler...")
115        signal.signal(signal.SIGTERM, self.sigtermHandler)
116        self.logdebug("SIGTERM handler installed.")
117       
118    def disableSigInt(self) :   
119        """Disables the SIGINT signal (which raises KeyboardInterrupt)."""
120        self.logdebug("Disabling SIGINT...")
121        self.oldSigIntHandler = signal.signal(signal.SIGINT, signal.SIG_IGN)
122        self.logdebug("SIGINT disabled.")
123       
124    def enableSigInt(self) :   
125        """Enables the SIGINT signal (which raises KeyboardInterrupt)."""
126        self.logdebug("Enabling SIGINT...")
127        signal.signal(signal.SIGINT, self.oldSigIntHandler)
128        self.logdebug("SIGINT enabled.")
129       
130    def waitForLock(self) :   
131        """Waits until we can acquire the lock file."""
132        self.logdebug("Waiting for lock...")
133        haslock = False
134        try :
135            while not haslock :
136                self.logdebug("Trying to acquire lock %s" % self.lockfilename)
137                try :
138                    self.lockfile = os.open(self.lockfilename, 
139                                            os.O_CREAT | os.O_EXCL | os.O_WRONLY, 
140                                            0600)
141                except OSError, error :                     
142                    if error.errno == errno.EEXIST :
143                        self.logdebug("Lock not available.")
144                    else :   
145                        self.logdebug("Lock not available : %s" % error)
146                    try :   
147                        try :   
148                            lockfobj = open(self.lockfilename, "r")   
149                            oldpid = int(lockfobj.readline().strip())
150                        except IOError :
151                            self.logdebug("Lock file has probably been deleted in the meantime.")
152                        except ValueError :   
153                            self.logdebug("Invalid content in existing lock file.")
154                        else :   
155                            try :
156                                # Is the old process still alive ?
157                                os.kill(oldpid, 0)
158                            except OSError :   
159                                # old process is dead, we delete the lock file
160                                self.logdebug("Old process (PID %i) is dead." % oldpid)
161                                os.unlink(self.lockfilename)
162                                self.logdebug("Lock file %s removed." % self.lockfilename)
163                            else :   
164                                self.logdebug("Old process (PID %i) still alive, waiting a bit..." % oldpid)
165                                time.sleep(0.25) # No hurry :)
166                    finally :   
167                        try :
168                            lockfobj.close()
169                        except NameError :   
170                            pass
171                else :   
172                    os.write(self.lockfile, str(self.pid))
173                    haslock = True   
174        except IOError :           
175            self.logdebug("I/O Error while waiting for lock.")
176        else :
177            self.logdebug("Lock acquired.")
178                   
179    def discoverOtherBackends(self) :   
180        """Discovers the other CUPS backends.
181       
182           Executes each existing backend in turn in device enumeration mode.
183           Returns the list of available backends.
184        """
185        # Unfortunately this method can't output any debug information
186        # to stdout or stderr, else CUPS considers that the device is
187        # not available.
188        available = []
189        (directory, myname) = os.path.split(sys.argv[0])
190        if not directory :
191            directory = "./"
192        tmpdir = tempfile.gettempdir()
193        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
194        if os.path.exists(lockfilename) :
195            lockfile = open(lockfilename, "r")
196            pid = int(lockfile.read())
197            lockfile.close()
198            try :
199                # see if the pid contained in the lock file is still running
200                os.kill(pid, 0)
201            except OSError, err :
202                if err.errno != errno.EPERM :
203                    # process doesn't exist anymore
204                    os.remove(lockfilename)
205           
206        if not os.path.exists(lockfilename) :
207            lockfile = open(lockfilename, "w")
208            lockfile.write("%i" % self.pid)
209            lockfile.close()
210            allbackends = [ os.path.join(directory, b) \
211                                for b in os.listdir(directory) \
212                                    if os.access(os.path.join(directory, b), os.X_OK) \
213                                        and (b != myname)] 
214            for backend in allbackends :                           
215                answer = os.popen(backend, "r")
216                try :
217                    devices = [line.strip() for line in answer.readlines()]
218                except :   
219                    devices = []
220                status = answer.close()
221                if status is None :
222                    for d in devices :
223                        # each line is of the form :
224                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
225                        # so we have to decompose it carefully
226                        fdevice = cStringIO.StringIO(d)
227                        tokenizer = shlex.shlex(fdevice)
228                        tokenizer.wordchars = tokenizer.wordchars + \
229                                                        r".:,?!~/\_$*-+={}[]()#"
230                        arguments = []
231                        while 1 :
232                            token = tokenizer.get_token()
233                            if token :
234                                arguments.append(token)
235                            else :
236                                break
237                        fdevice.close()
238                        try :
239                            (devicetype, device, name, fullname) = arguments
240                        except ValueError :   
241                            pass    # ignore this 'bizarre' device
242                        else :   
243                            if name.startswith('"') and name.endswith('"') :
244                                name = name[1:-1]
245                            if fullname.startswith('"') and fullname.endswith('"') :
246                                fullname = fullname[1:-1]
247                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
248                                                 % (devicetype, self.myname, \
249                                                    device, self.MyName, \
250                                                    name, self.MyName, \
251                                                    fullname))
252            os.remove(lockfilename)
253        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
254                             % (self.myname, self.MyName, self.MyName))
255        return available
256                       
257    def initBackendParameters(self) :   
258        """Initializes the backend's attributes."""
259        # check that the DEVICE_URI environment variable's value is
260        # prefixed with self.myname otherwise don't touch it.
261        # If this is the case, we have to remove the prefix from
262        # the environment before launching the real backend
263        self.logdebug("Initializing backend...")
264       
265        self.PrinterName = os.environ.get("PRINTER", "")
266        directories = [ self.config.getPrinterDirectory(self.PrinterName),
267                        "/tmp",
268                        "/var/tmp" ]
269        self.Directory = None
270        for direc in directories :
271            if os.access(direc, os.O_RDWR) :
272                self.Directory = direc
273                break
274            else :
275                self.printInfo("Insufficient permissions to access to temporary directory %s" % direc, "warn")
276               
277        self.Action = "ALLOW"   # job allowed by default
278        self.Reason = None
279        self.JobId = sys.argv[1].strip()
280        # use CUPS' user when printing test pages from CUPS' web interface
281        self.UserName = sys.argv[2].strip() or self.originalUserName or pwd.getpwuid(os.geteuid())[0]
282        self.OriginalUserName = self.UserName[:]
283        self.Title = sys.argv[3].strip()
284        self.Copies = int(sys.argv[4].strip())
285        self.Options = sys.argv[5].strip()
286        if len(sys.argv) == 7 :
287            self.InputFile = sys.argv[6] # read job's datas from file
288        else :   
289            self.InputFile = None        # read job's datas from stdin
290        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % \
291                   (self.myname, self.PrinterName, self.UserName, self.JobId))
292       
293        muststartwith = "%s:" % self.myname
294        device_uri = os.environ.get("DEVICE_URI", "")
295        if device_uri.startswith(muststartwith) :
296            fulldevice_uri = device_uri[:]
297            device_uri = fulldevice_uri[len(muststartwith):]
298            for i in range(2) :
299                if device_uri.startswith("/") : 
300                    device_uri = device_uri[1:]
301        try :
302            (backend, destination) = device_uri.split(":", 1) 
303        except ValueError :   
304            if not device_uri :
305                self.logdebug("Not attached to an existing print queue.")
306                backend = ""
307                printerhostname = ""
308            else :   
309                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
310        else :       
311            while destination.startswith("/") :
312                destination = destination[1:]
313            checkauth = destination.split("@", 1)   
314            if len(checkauth) == 2 :
315                destination = checkauth[1]
316            printerhostname = destination.split("/")[0].split(":")[0]
317           
318        self.PrinterHostName = printerhostname   
319        self.RealBackend = backend
320        self.DeviceURI = device_uri
321       
322        connerror = False
323        if haspkipplib :
324            self.ControlFile = "NotUsedAnymore"
325            self.logdebug("Querying CUPS server...")
326            cupsserver = pkipplib.CUPS() # TODO : username and password and/or encryption
327            answer = cupsserver.getJobAttributes(self.JobId)
328            if answer is None :
329                self.printInfo(_("Network error while querying the CUPS server : %s") \
330                                          % cupsserver.lastErrorMessage, "error")
331                connerror = True                       
332            else :   
333                self.logdebug("CUPS server answered without error.")
334                try :
335                    john = answer.job["job-originating-host-name"]
336                except KeyError :   
337                    try :
338                        john = answer.operation["job-originating-host-name"]
339                    except KeyError :   
340                        john = (None, None)
341                try :       
342                    jbing = answer.job["job-billing"]
343                except KeyError :   
344                    jbing = (None, None)
345                   
346        if connerror or not haspkipplib :       
347            (ippfilename, ippmessage) = self.parseIPPRequestFile()
348            self.ControlFile = ippfilename
349            john = ippmessage.operation_attributes.get("job-originating-host-name", \
350                   ippmessage.job_attributes.get("job-originating-host-name", \
351                   (None, None)))
352            jbing = ippmessage.job_attributes.get("job-billing", (None, None))
353               
354        if type(john) == type([]) : 
355            john = john[-1]
356        (chtype, self.ClientHost) = john 
357        if type(jbing) == type([]) : 
358            jbing = jbing[-1]
359        (jbtype, self.JobBillingCode) = jbing
360        if self.JobBillingCode is None :
361            self.OriginalJobBillingCode = None
362        else :   
363            self.JobBillingCode = self.UTF8ToUserCharset(self.JobBillingCode)
364            self.OriginalJobBillingCode = self.JobBillingCode[:]
365           
366        baselockfilename = self.DeviceURI.replace("/", ".")
367        baselockfilename = baselockfilename.replace(":", ".")
368        baselockfilename = baselockfilename.replace("?", ".")
369        baselockfilename = baselockfilename.replace("&", ".")
370        baselockfilename = baselockfilename.replace("@", ".")
371        self.lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, baselockfilename))
372       
373        self.logdebug("Backend : %s" % self.RealBackend)
374        self.logdebug("DeviceURI : %s" % self.DeviceURI)
375        self.logdebug("Printername : %s" % self.PrinterName)
376        self.logdebug("Username : %s" % self.UserName)
377        self.logdebug("JobId : %s" % self.JobId)
378        self.logdebug("Title : %s" % self.Title)
379        self.logdebug("Filename : %s" % self.InputFile)
380        self.logdebug("Copies : %s" % self.Copies)
381        self.logdebug("Options : %s" % self.Options)
382        self.logdebug("Directory : %s" % self.Directory) 
383        self.logdebug("DataFile : %s" % self.DataFile)
384        self.logdebug("ControlFile : %s" % self.ControlFile)
385        self.logdebug("JobBillingCode : %s" % self.JobBillingCode)
386        self.logdebug("JobOriginatingHostName : %s" % self.ClientHost)
387       
388        # fakes some entries to allow for external mailto
389        # before real entries are extracted from the database.
390        self.User = FakeUser(self.UserName)
391        self.Printer = FakePrinter(self.PrinterName)
392       
393        self.enableSigInt()
394        self.logdebug("Backend initialized.")
395       
396    def overwriteJobAttributes(self) :
397        """Overwrites some of the job's attributes if needed."""
398        self.logdebug("Sanitizing job's attributes...")
399        # First overwrite the job ticket
400        self.overwriteJobTicket()
401       
402        # do we want to strip out the Samba/Winbind domain name ?
403        separator = self.config.getWinbindSeparator()
404        if separator is not None :
405            self.UserName = self.UserName.split(separator)[-1]
406           
407        # do we want to lowercase usernames ?   
408        if self.config.getUserNameToLower() :
409            self.UserName = self.UserName.lower()
410           
411        # do we want to strip some prefix off of titles ?   
412        stripprefix = self.config.getStripTitle(self.PrinterName)
413        if stripprefix :
414            if fnmatch.fnmatch(self.Title[:len(stripprefix)], stripprefix) :
415                self.logdebug("Prefix [%s] removed from job's title [%s]." \
416                                      % (stripprefix, self.Title))
417                self.Title = self.Title[len(stripprefix):]
418               
419        self.logdebug("Username : %s" % self.UserName)
420        self.logdebug("BillingCode : %s" % self.JobBillingCode)
421        self.logdebug("Title : %s" % self.Title)
422        self.logdebug("Job's attributes sanitizing done.")
423               
424    def didUserConfirm(self) :
425        """Asks for user confirmation through an external script.
426       
427           returns False if the end user wants to cancel the job, else True.
428        """
429        self.logdebug("Checking if we have to ask for user's confirmation...")
430        answer = None                         
431        confirmationcommand = self.config.getAskConfirmation(self.PrinterName)
432        if confirmationcommand :
433            self.logdebug("Launching subprocess [%s] to ask for user confirmation." \
434                                     % confirmationcommand)
435            inputfile = os.popen(confirmationcommand, "r")
436            try :
437                for answer in inputfile.xreadlines() :
438                    answer = answer.strip().upper()
439                    if answer == "CANCEL" :
440                        break
441            except IOError, msg :           
442                self.logdebug("IOError while reading subprocess' output : %s" % msg)
443            inputfile.close()   
444            self.logdebug("User's confirmation received : %s" % (((answer == "CANCEL") and "CANCEL") or "CONTINUE"))
445        else :   
446            self.logdebug("No need to ask for user's confirmation, job processing will continue.")
447        return (answer != "CANCEL")   
448       
449    def overwriteJobTicket(self) :   
450        """Should we overwrite the job's ticket (username and billingcode) ?"""
451        self.logdebug("Checking if we need to overwrite the job ticket...")
452        jobticketcommand = self.config.getOverwriteJobTicket(self.PrinterName)
453        if jobticketcommand :
454            username = billingcode = action = None
455            self.logdebug("Launching subprocess [%s] to overwrite the job ticket." \
456                                     % jobticketcommand)
457            self.regainPriv()                         
458            inputfile = os.popen(jobticketcommand, "r")
459            try :
460                for line in inputfile.xreadlines() :
461                    line = line.strip()
462                    if line in ("DENY", "AUTH=NO", "AUTH=IMPOSSIBLE") :
463                        self.logdebug("Seen %s command." % line)
464                        action = "DENY"
465                    elif line == "CANCEL" :
466                        self.logdebug("Seen CANCEL command.")
467                        action = "CANCEL"
468                    elif line.startswith("USERNAME=") :   
469                        username = self.userCharsetToUTF8(line.split("=", 1)[1].strip())
470                        self.logdebug("Seen new username [%s]" % username)
471                    elif line.startswith("BILLINGCODE=") :   
472                        billingcode = self.userCharsetToUTF8(line.split("=", 1)[1].strip())
473                        self.logdebug("Seen new billing code [%s]" % billingcode)
474            except IOError, msg :           
475                self.logdebug("IOError while reading subprocess' output : %s" % msg)
476            inputfile.close()   
477            self.dropPriv()
478           
479            # now overwrite the job's ticket if new data was supplied
480            if action == "DENY" :
481                self.Action = action
482                self.Reason = _("You are not allowed to print at this time.")
483            elif action == "CANCEL" :
484                self.Action = action
485                self.Reason = _("Print job cancelled.")
486                os.environ["PYKOTASTATUS"] = "CANCELLED"
487            else :
488                # don't overwrite anything unless job authorized
489                # to continue to the physical printer.
490                if username and username.strip() :
491                    self.UserName = username
492                if billingcode is not None :
493                    self.JobBillingCode = billingcode 
494        self.logdebug("Job ticket overwriting done.")
495           
496    def saveDatasAndCheckSum(self) :
497        """Saves the input datas into a static file."""
498        self.logdebug("Duplicating data stream into %s" % self.DataFile)
499        mustclose = 0
500        outfile = open(self.DataFile, "wb")   
501        if self.InputFile is not None :
502            self.regainPriv()
503            infile = open(self.InputFile, "rb")
504            mustclose = 1
505        else :   
506            infile = sys.stdin
507        CHUNK = 64*1024         # read 64 Kb at a time
508        dummy = 0
509        sizeread = 0
510        checksum = md5.new()
511        while 1 :
512            data = infile.read(CHUNK) 
513            if not data :
514                break
515            sizeread += len(data)   
516            outfile.write(data)
517            checksum.update(data)   
518            if not (dummy % 32) : # Only display every 2 Mb
519                self.logdebug("%s bytes saved..." % sizeread)
520            dummy += 1   
521        if mustclose :   
522            infile.close()
523            self.dropPriv()
524           
525        outfile.close()
526        self.JobSizeBytes = sizeread   
527        self.JobMD5Sum = checksum.hexdigest()
528       
529        self.logdebug("JobSizeBytes : %s" % self.JobSizeBytes)
530        self.logdebug("JobMD5Sum : %s" % self.JobMD5Sum)
531        self.logdebug("Data stream duplicated into %s" % self.DataFile)
532           
533    def clean(self) :
534        """Cleans up the place."""
535        self.logdebug("Cleaning up...")
536        self.regainPriv()
537        self.deinstallSigTermHandler()
538        if (self.DataFile is not None) and os.path.exists(self.DataFile) :
539            if not self.config.getPrinterKeepFiles(self.PrinterName) :
540                self.logdebug("Work file %s will be deleted." % self.DataFile)
541                try :
542                    os.remove(self.DataFile)
543                except OSError, msg :
544                    self.logdebug("Problem while deleting work file %s : %s" % (self.DataFile, msg))
545                else :
546                    self.logdebug("Work file %s has been deleted." % self.DataFile)
547            else :   
548                self.logdebug("Work file %s will be kept." % self.DataFile)
549        PyKotaTool.clean(self)   
550        if (self.lockfile is not None) and os.path.exists(self.lockfilename) :
551            self.logdebug("Removing lock...")
552            try :
553                os.close(self.lockfile)
554                os.unlink(self.lockfilename)
555            except :   
556                self.logdebug("Problem while removing lock file %s" % self.lockfilename)
557            else :   
558                self.logdebug("Lock file %s removed." % self.lockfilename)
559        self.logdebug("Clean.")
560           
561    def precomputeJobSize(self) :   
562        """Computes the job size with a software method."""
563        self.logdebug("Precomputing job's size...")
564        self.preaccounter.beginJob(None)
565        self.preaccounter.endJob(None)
566        self.softwareJobSize = self.preaccounter.getJobSize(None)
567        self.logdebug("Precomputed job's size is %s pages." % self.softwareJobSize)
568       
569    def precomputeJobPrice(self) :   
570        """Precomputes the job price with a software method."""
571        self.logdebug("Precomputing job's price...")
572        self.softwareJobPrice = self.UserPQuota.computeJobPrice(self.softwareJobSize)
573        self.logdebug("Precomputed job's price is %.3f credits." \
574                                   % self.softwareJobPrice)
575       
576    def getCupsConfigDirectives(self, directives=[]) :
577        """Retrieves some CUPS directives from its configuration file.
578       
579           Returns a mapping with lowercased directives as keys and
580           their setting as values.
581        """
582        self.logdebug("Parsing CUPS' configuration file...")
583        dirvalues = {} 
584        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
585        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
586        try :
587            conffile = open(cupsdconf, "r")
588        except IOError :   
589            raise PyKotaToolError, "Unable to open %s" % cupsdconf
590        else :   
591            for line in conffile.readlines() :
592                linecopy = line.strip().lower()
593                for di in [d.lower() for d in directives] :
594                    if linecopy.startswith("%s " % di) :
595                        try :
596                            val = line.split()[1]
597                        except :   
598                            pass # ignore errors, we take the last value in any case.
599                        else :   
600                            dirvalues[di] = val
601            conffile.close()           
602        self.logdebug("CUPS' configuration file parsed successfully.")
603        return dirvalues       
604           
605    def parseIPPRequestFile(self) :       
606        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
607        self.logdebug("Parsing IPP request file...")
608       
609        class DummyClass :
610            """Class used to avoid errors."""
611            operation_attributes = {}
612            job_attributes = {}
613           
614        ippmessage = DummyClass() # in case the code below fails
615       
616        self.regainPriv()
617        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"])
618        requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
619        if (len(self.JobId) < 5) and self.JobId.isdigit() :
620            ippmessagefile = "c%05i" % int(self.JobId)
621        else :   
622            ippmessagefile = "c%s" % self.JobId
623        ippmessagefile = os.path.join(requestroot, ippmessagefile)
624        try :
625            ippdatafile = open(ippmessagefile)
626        except :   
627            self.logdebug("Unable to open IPP request file %s" % ippmessagefile)
628        else :   
629            self.logdebug("Parsing of IPP request file %s begins." % ippmessagefile)
630            try :
631                ippmessage = oldIPPRequest(ippdatafile.read())
632                ippmessage.parse()
633            except oldIPPError, msg :   
634                self.printInfo("Error while parsing %s : %s" \
635                                      % (ippmessagefile, msg), "warn")
636            else :   
637                self.logdebug("Parsing of IPP request file %s ends." \
638                                       % ippmessagefile)
639            ippdatafile.close()
640        self.dropPriv()
641        self.logdebug("IPP request file parsed successfully.")
642        return (ippmessagefile, ippmessage)
643               
644    def exportJobInfo(self) :   
645        """Exports the actual job's attributes to the environment."""
646        self.logdebug("Exporting job information to the environment...")
647        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
648        os.environ["PYKOTAPRINTERNAME"] = self.PrinterName
649        os.environ["PYKOTADIRECTORY"] = self.Directory
650        os.environ["PYKOTADATAFILE"] = self.DataFile
651        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.JobSizeBytes)
652        os.environ["PYKOTAMD5SUM"] = self.JobMD5Sum
653        os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = self.ClientHost or ""
654        os.environ["PYKOTAJOBID"] = self.JobId
655        os.environ["PYKOTAUSERNAME"] = self.UserName
656        os.environ["PYKOTAORIGINALUSERNAME"] = self.OriginalUserName
657        os.environ["PYKOTATITLE"] = self.Title
658        os.environ["PYKOTACOPIES"] = str(self.Copies)
659        os.environ["PYKOTAOPTIONS"] = self.Options
660        os.environ["PYKOTAFILENAME"] = self.InputFile or ""
661        os.environ["PYKOTAJOBBILLING"] = self.JobBillingCode or ""
662        os.environ["PYKOTAORIGINALJOBBILLING"] = self.OriginalJobBillingCode or ""
663        os.environ["PYKOTACONTROLFILE"] = self.ControlFile
664        os.environ["PYKOTAPRINTERHOSTNAME"] = self.PrinterHostName
665        os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize)
666        self.logdebug("Environment updated.")
667       
668    def exportUserInfo(self) :
669        """Exports user information to the environment."""
670        self.logdebug("Exporting user information to the environment...")
671        os.environ["PYKOTAOVERCHARGE"] = str(self.User.OverCharge)
672        os.environ["PYKOTALIMITBY"] = str(self.User.LimitBy)
673        os.environ["PYKOTABALANCE"] = str(self.User.AccountBalance or 0.0)
674        os.environ["PYKOTALIFETIMEPAID"] = str(self.User.LifeTimePaid or 0.0)
675        os.environ["PYKOTAUSERDESCRIPTION"] = str(self.User.Description or "")
676       
677        os.environ["PYKOTAPAGECOUNTER"] = str(self.UserPQuota.PageCounter or 0)
678        os.environ["PYKOTALIFEPAGECOUNTER"] = str(self.UserPQuota.LifePageCounter or 0)
679        os.environ["PYKOTASOFTLIMIT"] = str(self.UserPQuota.SoftLimit)
680        os.environ["PYKOTAHARDLIMIT"] = str(self.UserPQuota.HardLimit)
681        os.environ["PYKOTADATELIMIT"] = str(self.UserPQuota.DateLimit)
682        os.environ["PYKOTAWARNCOUNT"] = str(self.UserPQuota.WarnCount)
683       
684        # TODO : move this elsewhere once software accounting is done only once.
685        os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
686       
687        self.logdebug("Environment updated.")
688       
689    def exportPrinterInfo(self) :
690        """Exports printer information to the environment."""
691        self.logdebug("Exporting printer information to the environment...")
692        # exports the list of printers groups the current
693        # printer is a member of
694        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(self.Printer)])
695        os.environ["PYKOTAPRINTERDESCRIPTION"] = str(self.Printer.Description or "")
696        os.environ["PYKOTAPRINTERMAXJOBSIZE"] = str(self.Printer.MaxJobSize or _("Unlimited"))
697        os.environ["PYKOTAPRINTERPASSTHROUGHMODE"] = (self.Printer.PassThrough and _("ON")) or _("OFF")
698        os.environ["PYKOTAPRICEPERPAGE"] = str(self.Printer.PricePerPage or 0)
699        os.environ["PYKOTAPRICEPERJOB"] = str(self.Printer.PricePerJob or 0)
700        self.logdebug("Environment updated.")
701       
702    def exportPhaseInfo(self, phase) :
703        """Exports phase information to the environment."""
704        self.logdebug("Exporting phase information [%s] to the environment..." % phase)
705        os.environ["PYKOTAPHASE"] = phase
706        self.logdebug("Environment updated.")
707       
708    def exportJobSizeAndPrice(self) :
709        """Exports job's size and price information to the environment."""
710        self.logdebug("Exporting job's size and price information to the environment...")
711        os.environ["PYKOTAJOBSIZE"] = str(self.JobSize)
712        os.environ["PYKOTAJOBPRICE"] = str(self.JobPrice)
713        self.logdebug("Environment updated.")
714       
715    def exportReason(self) :
716        """Exports the job's action status and optional reason."""
717        self.logdebug("Exporting job's action status...")
718        os.environ["PYKOTAACTION"] = str(self.Action)
719        if self.Reason :
720            os.environ["PYKOTAREASON"] = str(self.Reason)
721        self.logdebug("Environment updated.")
722       
723    def acceptJob(self) :       
724        """Returns the appropriate exit code to tell CUPS all is OK."""
725        return 0
726           
727    def removeJob(self) :           
728        """Returns the appropriate exit code to let CUPS think all is OK.
729       
730           Returning 0 (success) prevents CUPS from stopping the print queue.
731        """   
732        return 0
733       
734    def launchPreHook(self) :
735        """Allows plugging of an external hook before the job gets printed."""
736        prehook = self.config.getPreHook(self.PrinterName)
737        if prehook :
738            self.logdebug("Executing pre-hook [%s]..." % prehook)
739            retcode = os.system(prehook)
740            self.logdebug("pre-hook exited with status %s." % retcode)
741       
742    def launchPostHook(self) :
743        """Allows plugging of an external hook after the job gets printed and/or denied."""
744        posthook = self.config.getPostHook(self.PrinterName)
745        if posthook :
746            self.logdebug("Executing post-hook [%s]..." % posthook)
747            retcode = os.system(posthook)
748            self.logdebug("post-hook exited with status %s." % retcode)
749           
750    def improveMessage(self, message) :       
751        """Improves a message by adding more informations in it if possible."""
752        try :
753            return "%s@%s(%s) => %s" % (self.UserName, \
754                                        self.PrinterName, \
755                                        self.JobId, \
756                                        message)
757        except :                                               
758            return message
759       
760    def logdebug(self, message) :       
761        """Improves the debug message before outputting it."""
762        PyKotaTool.logdebug(self, self.improveMessage(message))
763       
764    def printInfo(self, message, level="info") :       
765        """Improves the informational message before outputting it."""
766        self.logger.log_message(self.improveMessage(message), level)
767   
768    def startingBanner(self, withaccounting) :
769        """Retrieves a starting banner for current printer and returns its content."""
770        self.logdebug("Retrieving starting banner...")
771        self.printBanner(self.config.getStartingBanner(self.PrinterName), withaccounting)
772        self.logdebug("Starting banner retrieved.")
773   
774    def endingBanner(self, withaccounting) :
775        """Retrieves an ending banner for current printer and returns its content."""
776        self.logdebug("Retrieving ending banner...")
777        self.printBanner(self.config.getEndingBanner(self.PrinterName), withaccounting)
778        self.logdebug("Ending banner retrieved.")
779       
780    def printBanner(self, bannerfileorcommand, withaccounting) :
781        """Reads a banner or generates one through an external command.
782       
783           Returns the banner's content in a format which MUST be accepted
784           by the printer.
785        """
786        self.logdebug("Printing banner...")
787        if bannerfileorcommand :
788            if os.access(bannerfileorcommand, os.X_OK) or \
789                  not os.path.isfile(bannerfileorcommand) :
790                self.logdebug("Launching %s to generate a banner." % bannerfileorcommand)
791                child = popen2.Popen3(bannerfileorcommand, capturestderr=1)
792                self.runOriginalBackend(child.fromchild, isBanner=1)
793                child.tochild.close()
794                child.childerr.close()
795                child.fromchild.close()
796                status = child.wait()
797                if os.WIFEXITED(status) :
798                    status = os.WEXITSTATUS(status)
799                self.printInfo(_("Banner generator %s exit code is %s") \
800                                         % (bannerfileorcommand, str(status)))
801                if withaccounting :
802                    if self.accounter.isSoftware :
803                        self.BannerSize += 1 # TODO : fix this by passing the banner's content through software accounting
804            else :
805                self.logdebug("Using %s as the banner." % bannerfileorcommand)
806                try :
807                    fh = open(bannerfileorcommand, 'rb')
808                except IOError, msg :   
809                    self.printInfo("Impossible to open %s : %s" \
810                                       % (bannerfileorcommand, msg), "error")
811                else :   
812                    self.runOriginalBackend(fh, isBanner=1)
813                    fh.close()
814                    if withaccounting :
815                        if self.accounter.isSoftware :
816                            self.BannerSize += 1 # TODO : fix this by passing the banner's content through software accounting
817        self.logdebug("Banner printed...")
818               
819    def handleBanner(self, bannertype, withaccounting) :
820        """Handles the banner with or without accounting."""
821        if withaccounting :
822            acc = "with"
823        else :   
824            acc = "without"
825        self.logdebug("Handling %s banner %s accounting..." % (bannertype, acc))
826        if (self.Action == 'DENY') and \
827           (self.UserPQuota.WarnCount >= \
828                            self.config.getMaxDenyBanners(self.PrinterName)) :
829            self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), \
830                             "warn")
831        else :
832            if self.Action == 'DENY' :
833                self.logdebug("Incrementing the number of deny banners for user %s on printer %s" \
834                                  % (self.UserName, self.PrinterName))
835                self.UserPQuota.incDenyBannerCounter() # increments the warning counter
836                self.exportUserInfo()
837            getattr(self, "%sBanner" % bannertype)(withaccounting)
838        self.logdebug("%s banner done." % bannertype.title())
839       
840    def sanitizeJobSize(self) :   
841        """Sanitizes the job's size if needed."""
842        # TODO : there's a difficult to see bug here when banner accounting is activated and hardware accounting is used.
843        self.logdebug("Sanitizing job's size...")
844        if self.softwareJobSize and (self.JobSize != self.softwareJobSize) :
845            self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % \
846                                       (self.JobSize, self.softwareJobSize), \
847                           "error")
848            (limit, replacement) = self.config.getTrustJobSize(self.PrinterName)
849            if limit is None :
850                self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn")
851            else :
852                if self.JobSize <= limit :
853                    self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn")
854                else :
855                    self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn")
856                    if replacement == "PRECOMPUTED" :
857                        self.JobSize = self.softwareJobSize
858                    else :   
859                        self.JobSize = replacement
860        self.logdebug("Job's size sanitized.")
861                       
862    def getPrinterUserAndUserPQuota(self) :       
863        """Returns a tuple (policy, printer, user, and user print quota) on this printer.
864       
865           "OK" is returned in the policy if both printer, user and user print quota
866           exist in the Quota Storage.
867           Otherwise, the policy as defined for this printer in pykota.conf is returned.
868           
869           If policy was set to "EXTERNAL" and one of printer, user, or user print quota
870           doesn't exist in the Quota Storage, then an external command is launched, as
871           defined in the external policy for this printer in pykota.conf
872           This external command can do anything, like automatically adding printers
873           or users, for example, and finally extracting printer, user and user print
874           quota from the Quota Storage is tried a second time.
875           
876           "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status
877           was returned by the external command.
878        """
879        self.logdebug("Retrieving printer, user, and user print quota entry from database...")
880        for passnumber in range(1, 3) :
881            printer = self.storage.getPrinter(self.PrinterName)
882            user = self.storage.getUser(self.UserName)
883            userpquota = self.storage.getUserPQuota(user, printer)
884            if printer.Exists and user.Exists and userpquota.Exists :
885                policy = "OK"
886                break
887            (policy, args) = self.config.getPrinterPolicy(self.PrinterName)
888            if policy == "EXTERNAL" :   
889                commandline = self.formatCommandLine(args, user, printer)
890                if not printer.Exists :
891                    self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.PrinterName, commandline, self.PrinterName))
892                if not user.Exists :
893                    self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.UserName, commandline, self.PrinterName))
894                if not userpquota.Exists :
895                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying external policy (%s) for printer %s") % (self.UserName, self.PrinterName, commandline, self.PrinterName))
896                if os.system(commandline) :
897                    self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.PrinterName), "error")
898                    policy = "EXTERNALERROR"
899                    break
900            else :       
901                if not printer.Exists :
902                    self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.PrinterName, policy))
903                if not user.Exists :
904                    self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.UserName, policy, self.PrinterName))
905                if not userpquota.Exists :
906                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.UserName, self.PrinterName, policy))
907                break
908               
909        if policy == "EXTERNAL" :   
910            if not printer.Exists :
911                self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.PrinterName)
912            if not user.Exists :
913                self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.UserName, self.PrinterName))
914            if not userpquota.Exists :
915                self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.UserName, self.PrinterName))
916        self.Policy = policy         
917        self.Printer = printer
918        self.User = user
919        self.UserPQuota = userpquota
920        self.logdebug("Retrieval of printer, user and user print quota entry done.")
921       
922    def getBillingCode(self) :   
923        """Extracts the billing code from the database.
924         
925           An optional script is launched to notify the user when
926           the billing code is unknown and PyKota was configured to
927           deny printing in this case.
928        """
929        self.logdebug("Retrieving billing code information from the database...")
930        self.BillingCode = None
931        if self.JobBillingCode :
932            self.BillingCode = self.storage.getBillingCode(self.JobBillingCode)
933            if self.BillingCode.Exists :
934                self.logdebug("Billing code [%s] found in database." % self.JobBillingCode)
935            else :
936                msg = "Unknown billing code [%s] : " % self.JobBillingCode
937                (newaction, script) = self.config.getUnknownBillingCode(self.PrinterName)
938                if newaction == "CREATE" :
939                    self.logdebug(msg + "will be created.")
940                    self.storage.addBillingCode(self.BillingCode)
941                    self.BillingCode = self.storage.getBillingCode(self.JobBillingCode)
942                    if self.BillingCode.Exists :
943                        self.logdebug(msg + "has been created.")
944                    else :   
945                        self.printInfo(msg + "couldn't be created.", "error")
946                else :   
947                    self.logdebug(msg + "job will be denied.")
948                    self.Action = newaction
949                    if script is not None : 
950                        self.logdebug(msg + "launching subprocess [%s] to notify user." % script)
951                        os.system(script)
952        self.logdebug("Retrieval of billing code information done.")
953       
954    def checkIfDupe(self) :   
955        """Checks if the job is a duplicate, and handles the situation."""
956        self.logdebug("Checking if the job is a duplicate...")
957        denyduplicates = self.config.getDenyDuplicates(self.PrinterName)
958        if not denyduplicates :
959            self.logdebug("We don't care about duplicate jobs after all.")
960        else :
961            if self.Printer.LastJob.Exists \
962                    and (self.Printer.LastJob.UserName == self.UserName) \
963                    and (self.Printer.LastJob.JobMD5Sum == self.JobMD5Sum) :
964                now = DateTime.now()
965                try :
966                    previous = DateTime.ISO.ParseDateTime(str(self.Printer.LastJob.JobDate)).localtime()
967                except :
968                    previous = now
969                difference = (now - previous).seconds
970                duplicatesdelay = self.config.getDuplicatesDelay(self.PrinterName)
971                self.logdebug("Difference with previous job : %.2f seconds. Duplicates delay : %.2f seconds." % (difference, duplicatesdelay))
972                if difference > duplicatesdelay :
973                    self.logdebug("Duplicate job allowed because previous one is more than %.2f seconds old." % duplicatesdelay)
974                else :
975                    # TODO : use the current user's last job instead of 
976                    # TODO : the current printer's last job. This would be
977                    # TODO : better but requires an additional database query
978                    # TODO : with SQL, and is much more complex with the
979                    # TODO : actual LDAP schema. Maybe this is not very
980                    # TODO : important, because usually duplicate jobs are sucessive.
981                    msg = _("Job is a dupe")
982                    if denyduplicates == 1 :
983                        self.printInfo("%s : %s." % (msg, _("Printing is denied by configuration")), "warn")
984                        self.Action = "DENY"
985                        self.Reason = _("Duplicate print jobs are not allowed on printer %s.") % self.PrinterName
986                    else :   
987                        self.logdebug("Launching subprocess [%s] to see if duplicate jobs should be allowed or not." % denyduplicates)
988                        fanswer = os.popen(denyduplicates, "r")
989                        self.Action = fanswer.read().strip().upper()
990                        fanswer.close()
991                        if self.Action == "DENY" :     
992                            self.printInfo("%s : %s." % (msg, _("Subprocess denied printing of a dupe")), "warn")
993                            self.Reason = _("Duplicate print jobs are not allowed on printer %s at this time.") % self.PrinterName
994                        else :   
995                            self.printInfo("%s : %s." % (msg, _("Subprocess allowed printing of a dupe")), "warn")
996            else :           
997                self.logdebug("Job doesn't seem to be a duplicate.")
998        self.logdebug("Checking if the job is a duplicate done.")
999       
1000    def tellUser(self) :
1001        """Sends a message to an user."""
1002        self.logdebug("Sending some feedback to user %s..." % self.UserName) 
1003        if not self.Reason :
1004            self.logdebug("No feedback to send to user %s." % self.UserName)
1005        else :   
1006            (mailto, arguments) = self.config.getMailTo(self.PrinterName)
1007            if mailto == "EXTERNAL" :
1008                # TODO : clean this again
1009                self.regainPriv()
1010                self.externalMailTo(arguments, self.Action, self.User, self.Printer, self.Reason)
1011                self.dropPriv()
1012            else :   
1013                # TODO : clean this again
1014                admin = self.config.getAdmin(self.PrinterName)
1015                adminmail = self.config.getAdminMail(self.PrinterName)
1016                usermail = self.User.Email or self.User.Name
1017                if "@" not in usermail :
1018                    usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver)
1019                destination = []
1020                if mailto in ("BOTH", "ADMIN") :
1021                    destination.append(adminmail)
1022                if mailto in ("BOTH", "USER") :   
1023                    destination.append(usermail)
1024                   
1025                fullmessage = self.Reason + (_("\n\nYour system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail))
1026                try :   
1027                    server = smtplib.SMTP(self.smtpserver)
1028                except socket.error, msg :   
1029                    self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
1030                else :
1031                    try :
1032                        msg = MIMEText(fullmessage, _charset=self.charset)
1033                        msg["Subject"] = str(Header(_("Print Quota"), charset=self.charset))
1034                        msg["From"] = adminmail
1035                        if mailto in ("BOTH", "USER") :
1036                            msg["To"] = usermail
1037                            if mailto == "BOTH" :
1038                                msg["Cc"] = adminmail
1039                        else :   
1040                            msg["To"] = adminmail
1041                        msg["Date"] = email.Utils.formatdate(localtime=True)
1042                        server.sendmail(adminmail, destination, msg.as_string())
1043                    except smtplib.SMTPException, answer :   
1044                        try :
1045                            for (k, v) in answer.recipients.items() :
1046                                self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
1047                        except AttributeError :
1048                            self.printInfo(_("Problem when sending mail : %s") % str(answer), "error")
1049                    server.quit()
1050            self.logdebug("Feedback sent to user %s." % self.UserName)
1051               
1052    def mainWork(self) :   
1053        """Main work is done here."""
1054        if not self.JobSizeBytes :
1055            # if no data to pass to real backend, probably a filter
1056            # higher in the chain failed because of a misconfiguration.
1057            # we deny the job in this case (nothing to print anyway)
1058            self.Reason = _("Job contains no data. Printing is denied.")
1059            self.printInfo(self.Reason, "error")
1060            self.tellUser()
1061            return self.removeJob()
1062           
1063        self.getPrinterUserAndUserPQuota()
1064        if self.Policy == "EXTERNALERROR" :
1065            # Policy was 'EXTERNAL' and the external command returned an error code
1066            self.Reason = _("Error in external policy script. Printing is denied.")
1067            self.printInfo(self.Reason, "error")
1068            self.tellUser()
1069            return self.removeJob()
1070        elif self.Policy == "EXTERNAL" :
1071            # Policy was 'EXTERNAL' and the external command wasn't able
1072            # to add either the printer, user or user print quota
1073            self.Reason = _("Still no print quota entry for user %s on printer %s after external policy. Printing is denied.") % (self.UserName, self.PrinterName)
1074            self.printInfo(self.Reason, "warn")
1075            self.tellUser()
1076            return self.removeJob()
1077        elif self.Policy == "DENY" :   
1078            # Either printer, user or user print quota doesn't exist,
1079            # and the job should be rejected.
1080            self.Reason = _("Printing is denied by printer policy.")
1081            self.printInfo(self.Reason, "warn")
1082            self.tellUser()
1083            return self.removeJob()
1084        elif self.Policy == "ALLOW" :
1085            # ALLOW means : Either printer, user or user print quota doesn't exist,
1086            #               but the job should be allowed anyway.
1087            self.Reason = _("Job allowed by printer policy. No accounting will be done.")
1088            self.printInfo(self.Reason, "warn")
1089            self.tellUser()
1090            return self.printJobDatas()
1091        elif self.Policy == "OK" :
1092            # OK means : Both printer, user and user print quota exist, job should
1093            #            be allowed if current user is allowed to print on this printer
1094            return self.doWork()
1095        else :   
1096            self.Reason = _("Invalid policy %s for printer %s") % (self.Policy, self.PrinterName)
1097            self.printInfo(self.Reason, "error")
1098            self.tellUser()
1099            return self.removeJob()
1100   
1101    def doWork(self) :   
1102        """The accounting work is done here."""
1103        self.precomputeJobPrice()
1104        self.exportUserInfo()
1105        self.exportPrinterInfo()
1106        self.exportPhaseInfo("BEFORE")
1107       
1108        if self.Action not in ("DENY", "CANCEL") : 
1109            if not self.didUserConfirm() :
1110                self.Action = "CANCEL"
1111                self.Reason = _("Print job cancelled.")
1112                os.environ["PYKOTASTATUS"] = "CANCELLED"
1113               
1114        if self.Action not in ("DENY", "CANCEL") : 
1115            if self.Printer.MaxJobSize and (self.softwareJobSize > self.Printer.MaxJobSize) :
1116                # This printer was set to refuse jobs this large.
1117                self.printInfo(_("Precomputed job size (%s pages) too large for printer %s.") % (self.softwareJobSize, self.PrinterName), "warn")
1118                self.Action = "DENY"
1119                # here we don't put the precomputed job size in the message
1120                # because in case of error the user could complain :-)
1121                self.Reason = _("You are not allowed to print so many pages on printer %s at this time.") % self.PrinterName
1122           
1123        if self.Action not in ("DENY", "CANCEL") :
1124            if self.User.LimitBy == "noprint" :
1125                self.printInfo(_("User %s is not allowed to print at this time.") % self.UserName, "warn")
1126                self.Action = "DENY"
1127                self.Reason = _("Your account settings forbid you to print at this time.")
1128               
1129        if self.Action not in ("DENY", "CANCEL") :
1130            # If printing is still allowed at this time, we
1131            # need to extract the billing code information from the database.
1132            # No need to do this if the job is denied, this way we
1133            # save some database queries.
1134            self.getBillingCode()
1135           
1136        if self.Action not in ("DENY", "CANCEL") :
1137            # If printing is still allowed at this time, we
1138            # need to check if the job is a dupe or not, and what to do then.
1139            # No need to do this if the job is denied, this way we
1140            # save some database queries.
1141            self.checkIfDupe()
1142                   
1143        if self.Action not in ("DENY", "CANCEL") :
1144            # If printing is still allowed at this time, we
1145            # need to check the user's print quota on the current printer.
1146            # No need to do this if the job is denied, this way we
1147            # save some database queries.
1148            if self.User.LimitBy in ('noquota', 'nochange') :
1149                self.logdebug("User %s is allowed to print with no limit, no need to check quota." % self.UserName)
1150            elif self.Printer.PassThrough :   
1151                self.logdebug("Printer %s is in PassThrough mode, no need to check quota." % self.PrinterName)
1152            else :
1153                self.logdebug("Checking user %s print quota entry on printer %s" \
1154                                    % (self.UserName, self.PrinterName))
1155                self.Action = self.checkUserPQuota(self.UserPQuota)
1156                if self.Action.startswith("POLICY_") :
1157                    self.Action = self.Action[7:]
1158                if self.Action == "DENY" :
1159                    self.printInfo(_("Print Quota exceeded for user %s on printer %s") % (self.UserName, self.PrinterName))
1160                    self.Reason = self.config.getHardWarn(self.PrinterName)
1161                elif self.Action == "WARN" :   
1162                    self.printInfo(_("Print Quota low for user %s on printer %s") % (self.UserName, self.PrinterName))
1163                    if self.User.LimitBy and (self.User.LimitBy.lower() == "balance") : 
1164                        self.Reason = self.config.getPoorWarn()
1165                    else :     
1166                        self.Reason = self.config.getSoftWarn(self.PrinterName)
1167           
1168        # exports some new environment variables
1169        self.exportReason()
1170       
1171        # now tell the user if he needs to know something
1172        self.tellUser()
1173       
1174        # launches the pre hook
1175        self.launchPreHook()
1176       
1177        # handle starting banner pages without accounting
1178        self.BannerSize = 0
1179        accountbanner = self.config.getAccountBanner(self.PrinterName)
1180        if accountbanner in ["ENDING", "NONE"] :
1181            self.handleBanner("starting", 0)
1182       
1183        if self.Action == "DENY" :
1184            self.printInfo(_("Job denied, no accounting will be done."))
1185        elif self.Action == "CANCEL" :   
1186            self.printInfo(_("Job cancelled, no accounting will be done."))
1187        else :
1188            self.printInfo(_("Job accounting begins."))
1189            self.deinstallSigTermHandler()
1190            self.accounter.beginJob(self.Printer)
1191            self.installSigTermHandler()
1192       
1193        # handle starting banner pages with accounting
1194        if accountbanner in ["STARTING", "BOTH"] :
1195            if not self.gotSigTerm :
1196                self.handleBanner("starting", 1)
1197       
1198        # pass the job's data to the real backend   
1199        if (not self.gotSigTerm) and (self.Action in ["ALLOW", "WARN"]) :
1200            retcode = self.printJobDatas()
1201        else :       
1202            retcode = self.removeJob()
1203       
1204        # indicate phase change
1205        self.exportPhaseInfo("AFTER")
1206       
1207        # handle ending banner pages with accounting
1208        if accountbanner in ["ENDING", "BOTH"] :
1209            if not self.gotSigTerm :
1210                self.handleBanner("ending", 1)
1211       
1212        # stops accounting
1213        if self.Action == "DENY" :
1214            self.printInfo(_("Job denied, no accounting has been done."))
1215        elif self.Action == "CANCEL" :   
1216            self.printInfo(_("Job cancelled, no accounting has been done."))
1217        else :
1218            self.deinstallSigTermHandler()
1219            self.accounter.endJob(self.Printer)
1220            self.installSigTermHandler()
1221            self.printInfo(_("Job accounting ends."))
1222       
1223        # Do all these database changes within a single transaction   
1224        # NB : we don't enclose ALL the changes within a single transaction
1225        # because while waiting for the printer to answer its internal page
1226        # counter, we would open the door to accounting problems for other
1227        # jobs launched by the same user at the same time on other printers.
1228        # All the code below doesn't take much time, so it's fine.
1229        self.storage.beginTransaction()
1230        try :
1231            onbackenderror = self.config.getPrinterOnBackendError(self.PrinterName)
1232            if retcode :
1233                # NB : We don't send any feedback to the end user. Only the admin
1234                # has to know that the real CUPS backend failed.
1235                self.Action = "PROBLEM"
1236                self.exportReason()
1237                if "NOCHARGE" in onbackenderror :
1238                    self.JobSize = 0
1239                    self.printInfo(_("Job size forced to 0 because the real CUPS backend failed. No accounting will be done."), "warn")
1240                else :   
1241                    self.printInfo(_("The real CUPS backend failed, but the job will be accounted for anyway."), "warn")
1242                   
1243            # retrieve the job size   
1244            if self.Action == "DENY" :
1245                self.JobSize = 0
1246                self.printInfo(_("Job size forced to 0 because printing is denied."))
1247            elif self.Action == "CANCEL" :     
1248                self.JobSize = 0
1249                self.printInfo(_("Job size forced to 0 because printing was cancelled."))
1250            else :   
1251                self.UserPQuota.resetDenyBannerCounter()
1252                if (self.Action != "PROBLEM") or ("CHARGE" in onbackenderror) : 
1253                    self.JobSize = self.accounter.getJobSize(self.Printer)
1254                    self.sanitizeJobSize()
1255                    self.JobSize += self.BannerSize
1256            self.printInfo(_("Job size : %i") % self.JobSize)
1257           
1258            if ((self.Action == "PROBLEM") and ("NOCHARGE" in onbackenderror)) or \
1259                (self.Action in ("DENY", "CANCEL")) :
1260                self.JobPrice = 0.0
1261            elif (self.User.LimitBy == "nochange") or self.Printer.PassThrough :
1262                # no need to update the quota for the current user on this printer
1263                self.printInfo(_("User %s's quota on printer %s won't be modified") % (self.UserName, self.PrinterName))
1264                self.JobPrice = 0.0
1265            else :
1266                # update the quota for the current user on this printer
1267                self.printInfo(_("Updating user %s's quota on printer %s") % (self.UserName, self.PrinterName))
1268                self.JobPrice = self.UserPQuota.increasePagesUsage(self.JobSize)
1269           
1270            # adds the current job to history   
1271            self.Printer.addJobToHistory(self.JobId, self.User, self.accounter.getLastPageCounter(), \
1272                                    self.Action, self.JobSize, self.JobPrice, self.InputFile, \
1273                                    self.Title, self.Copies, self.Options, self.ClientHost, \
1274                                    self.JobSizeBytes, self.JobMD5Sum, None, self.JobBillingCode, \
1275                                    self.softwareJobSize, self.softwareJobPrice)
1276            self.printInfo(_("Job added to history."))
1277           
1278            if hasattr(self, "BillingCode") and self.BillingCode and self.BillingCode.Exists :
1279                if (self.Action in ("ALLOW", "WARN")) or \
1280                   ((self.Action == "PROBLEM") and ("CHARGE" in onbackenderror)) :
1281                    self.BillingCode.consume(self.JobSize, self.JobPrice)
1282                    self.printInfo(_("Billing code %s was updated.") % self.BillingCode.BillingCode)
1283        except :   
1284            self.storage.rollbackTransaction()
1285            raise
1286        else :   
1287            self.storage.commitTransaction()
1288           
1289        # exports some new environment variables
1290        self.exportJobSizeAndPrice()
1291       
1292        # then re-export user information with new values
1293        self.exportUserInfo()
1294       
1295        # handle ending banner pages without accounting
1296        if accountbanner in ["STARTING", "NONE"] :
1297            self.handleBanner("ending", 0)
1298                   
1299        self.launchPostHook()
1300           
1301        return retcode   
1302               
1303    def printJobDatas(self) :           
1304        """Sends the job's datas to the real backend."""
1305        self.logdebug("Sending job's datas to real backend...")
1306       
1307        delay = 0
1308        number = 1
1309        for onb in self.config.getPrinterOnBackendError(self.PrinterName) :
1310            if onb.startswith("RETRY:") :
1311                try :
1312                    (number, delay) = [int(p) for p in onb[6:].split(":", 2)]
1313                    if (number < 0) or (delay < 0) :
1314                        raise ValueError
1315                except ValueError :   
1316                    self.printInfo(_("Incorrect value for the 'onbackenderror' directive in section [%s]") % self.PrinterName, "error")
1317                    delay = 0
1318                    number = 1
1319                else :   
1320                    break
1321        loopcnt = 1 
1322        while True :           
1323            if self.InputFile is None :
1324                infile = open(self.DataFile, "rb")
1325            else :   
1326                infile = None
1327            retcode = self.runOriginalBackend(infile)
1328            if self.InputFile is None :
1329                infile.close()
1330            if not retcode :
1331                break
1332            else :
1333                if (not number) or (loopcnt < number) :
1334                    self.logdebug(_("The real backend produced an error, we will try again in %s seconds.") % delay)
1335                    time.sleep(delay)
1336                    loopcnt += 1
1337                else :   
1338                    break
1339           
1340        self.logdebug("Job's datas sent to real backend.")
1341        return retcode
1342       
1343    def runOriginalBackend(self, filehandle=None, isBanner=0) :
1344        """Launches the original backend."""
1345        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1346        if not isBanner :
1347            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1348        else :   
1349            # For banners, we absolutely WANT
1350            # to remove any filename from the command line !
1351            self.logdebug("It looks like we try to print a banner.")
1352            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:6]
1353        arguments[2] = self.UserName # in case it was overwritten by external script
1354        # TODO : do something about job-billing option, in case it was overwritten as well...
1355        # TODO : do something about the job title : if we are printing a banner and the backend
1356        # TODO : uses the job's title to name an output file (cups-pdf:// for example), we're stuck !
1357       
1358        self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1359        self.regainPriv()   
1360        pid = os.fork()
1361        self.logdebug("Forked !")
1362        if pid == 0 :
1363            if filehandle is not None :
1364                self.logdebug("Redirecting file handle to real backend's stdin")
1365                os.dup2(filehandle.fileno(), 0)
1366            try :
1367                self.logdebug("Calling execve...")
1368                os.execve(originalbackend, arguments, os.environ)
1369            except OSError, msg :
1370                self.logdebug("execve() failed: %s" % msg)
1371            self.logdebug("We shouldn't be there !!!")   
1372            os._exit(-1)
1373        self.dropPriv()   
1374       
1375        self.logdebug("Waiting for original backend to exit...")   
1376        killed = 0
1377        status = -1
1378        while status == -1 :
1379            try :
1380                status = os.waitpid(pid, 0)[1]
1381            except OSError, (err, msg) :
1382                if (err == 4) and self.gotSigTerm :
1383                    os.kill(pid, signal.SIGTERM)
1384                    killed = 1
1385                   
1386        if os.WIFEXITED(status) :
1387            status = os.WEXITSTATUS(status)
1388            message = "CUPS backend %s returned %d." % \
1389                            (originalbackend, status)
1390            if status :
1391                level = "error"
1392                self.Reason = message
1393            else :   
1394                level = "info"
1395            self.printInfo(message, level)
1396            return status
1397        elif not killed :
1398            self.Reason = "CUPS backend %s died abnormally." % originalbackend
1399            self.printInfo(self.Reason, "error")
1400            return -1
1401        else :
1402            self.Reason = "CUPS backend %s was killed." % originalbackend
1403            self.printInfo(self.Reason, "warn")
1404            return 1
1405       
1406if __name__ == "__main__" :   
1407    # This is a CUPS backend, we should act and die like a CUPS backend
1408    wrapper = CUPSBackend()
1409    if len(sys.argv) == 1 :
1410        print "\n".join(wrapper.discoverOtherBackends())
1411        sys.exit(0)               
1412    elif len(sys.argv) not in (6, 7) :   
1413        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1414                              % sys.argv[0])
1415        sys.exit(1)
1416    else :   
1417        try :
1418            try :
1419                wrapper.deferredInit()
1420                wrapper.initBackendParameters()
1421                wrapper.waitForLock()
1422                if os.environ.get("PYKOTASTATUS") == "CANCELLED" :
1423                    raise KeyboardInterrupt
1424                wrapper.saveDatasAndCheckSum()
1425                wrapper.preaccounter = openAccounter(wrapper, ispreaccounter=1)
1426                wrapper.accounter = openAccounter(wrapper)
1427                wrapper.precomputeJobSize()
1428                wrapper.exportJobInfo() # exports a first time to give hints to external scripts
1429                wrapper.overwriteJobAttributes()
1430                wrapper.exportJobInfo() # re-exports in case it was overwritten
1431                retcode = wrapper.mainWork()
1432            except KeyboardInterrupt :   
1433                wrapper.printInfo(_("Job %s interrupted by the administrator !") % wrapper.JobId, "warn")
1434                retcode = 0
1435            except SystemExit, err :   
1436                retcode = err.code
1437            except :   
1438                try :
1439                    wrapper.crashed("cupspykota backend failed")
1440                except :   
1441                    crashed("cupspykota backend failed")
1442                retcode = 1
1443        finally :       
1444            wrapper.clean()
1445        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.