root / pykota / trunk / bin / cupspykota @ 3012

Revision 3012, 69.5 kB (checked in by jerome, 16 years ago)

Exports two additional environment variables.

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