root / pykota / trunk / bin / cupspykota @ 3124

Revision 3124, 69.6 kB (checked in by matt, 17 years ago)

If a job is cancelled (e.g. by pknotify/pykoticon), make printing start and end banners optional.

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