root / pykota / trunk / bin / cupspykota @ 2973

Revision 2973, 67.1 kB (checked in by jerome, 18 years ago)

In case there's a permission problem on the temporary directory, falls
back to /tmp or /var/tmp

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