root / pykota / trunk / bin / cupspykota @ 2992

Revision 2983, 67.3 kB (checked in by jerome, 18 years ago)

Fixed a problem in the handling of SMTP errors.

  • 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                        try :
1011                            for (k, v) in answer.recipients.items() :
1012                                self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
1013                        except AttributeError :
1014                            self.printInfo(_("Problem when sending mail : %s") % str(answer), "error")
1015                    server.quit()
1016            self.logdebug("Feedback sent to user %s." % self.UserName)
1017               
1018    def mainWork(self) :   
1019        """Main work is done here."""
1020        if not self.JobSizeBytes :
1021            # if no data to pass to real backend, probably a filter
1022            # higher in the chain failed because of a misconfiguration.
1023            # we deny the job in this case (nothing to print anyway)
1024            self.Reason = _("Job contains no data. Printing is denied.")
1025            self.printInfo(self.Reason, "error")
1026            self.tellUser()
1027            return self.removeJob()
1028           
1029        self.getPrinterUserAndUserPQuota()
1030        if self.Policy == "EXTERNALERROR" :
1031            # Policy was 'EXTERNAL' and the external command returned an error code
1032            self.Reason = _("Error in external policy script. Printing is denied.")
1033            self.printInfo(self.Reason, "error")
1034            self.tellUser()
1035            return self.removeJob()
1036        elif self.Policy == "EXTERNAL" :
1037            # Policy was 'EXTERNAL' and the external command wasn't able
1038            # to add either the printer, user or user print quota
1039            self.Reason = _("Still no print quota entry for user %s on printer %s after external policy. Printing is denied.") % (self.UserName, self.PrinterName)
1040            self.printInfo(self.Reason, "warn")
1041            self.tellUser()
1042            return self.removeJob()
1043        elif self.Policy == "DENY" :   
1044            # Either printer, user or user print quota doesn't exist,
1045            # and the job should be rejected.
1046            self.Reason = _("Printing is denied by printer policy.")
1047            self.printInfo(self.Reason, "warn")
1048            self.tellUser()
1049            return self.removeJob()
1050        elif self.Policy == "ALLOW" :
1051            # ALLOW means : Either printer, user or user print quota doesn't exist,
1052            #               but the job should be allowed anyway.
1053            self.Reason = _("Job allowed by printer policy. No accounting will be done.")
1054            self.printInfo(self.Reason, "warn")
1055            self.tellUser()
1056            return self.printJobDatas()
1057        elif self.Policy == "OK" :
1058            # OK means : Both printer, user and user print quota exist, job should
1059            #            be allowed if current user is allowed to print on this printer
1060            return self.doWork()
1061        else :   
1062            self.Reason = _("Invalid policy %s for printer %s") % (self.Policy, self.PrinterName)
1063            self.printInfo(self.Reason, "error")
1064            self.tellUser()
1065            return self.removeJob()
1066   
1067    def doWork(self) :   
1068        """The accounting work is done here."""
1069        self.precomputeJobPrice()
1070        self.exportUserInfo()
1071        self.exportPrinterInfo()
1072        self.exportPhaseInfo("BEFORE")
1073       
1074        if self.Action not in ("DENY", "CANCEL") : 
1075            if not self.didUserConfirm() :
1076                self.Action = "CANCEL"
1077                self.Reason = _("Print job cancelled.")
1078                os.environ["PYKOTASTATUS"] = "CANCELLED"
1079               
1080        if self.Action not in ("DENY", "CANCEL") : 
1081            if self.Printer.MaxJobSize and (self.softwareJobSize > self.Printer.MaxJobSize) :
1082                # This printer was set to refuse jobs this large.
1083                self.printInfo(_("Precomputed job size (%s pages) too large for printer %s.") % (self.softwareJobSize, self.PrinterName), "warn")
1084                self.Action = "DENY"
1085                # here we don't put the precomputed job size in the message
1086                # because in case of error the user could complain :-)
1087                self.Reason = _("You are not allowed to print so many pages on printer %s at this time.") % self.PrinterName
1088           
1089        if self.Action not in ("DENY", "CANCEL") :
1090            if self.User.LimitBy == "noprint" :
1091                self.printInfo(_("User %s is not allowed to print at this time.") % self.UserName, "warn")
1092                self.Action = "DENY"
1093                self.Reason = _("Your account settings forbid you to print at this time.")
1094               
1095        if self.Action not in ("DENY", "CANCEL") :
1096            # If printing is still allowed at this time, we
1097            # need to extract the billing code information from the database.
1098            # No need to do this if the job is denied, this way we
1099            # save some database queries.
1100            self.getBillingCode()
1101           
1102        if self.Action not in ("DENY", "CANCEL") :
1103            # If printing is still allowed at this time, we
1104            # need to check if the job is a dupe or not, and what to do then.
1105            # No need to do this if the job is denied, this way we
1106            # save some database queries.
1107            self.checkIfDupe()
1108                   
1109        if self.Action not in ("DENY", "CANCEL") :
1110            # If printing is still allowed at this time, we
1111            # need to check the user's print quota on the current printer.
1112            # No need to do this if the job is denied, this way we
1113            # save some database queries.
1114            if self.User.LimitBy in ('noquota', 'nochange') :
1115                self.logdebug("User %s is allowed to print with no limit, no need to check quota." % self.UserName)
1116            elif self.Printer.PassThrough :   
1117                self.logdebug("Printer %s is in PassThrough mode, no need to check quota." % self.PrinterName)
1118            else :
1119                self.logdebug("Checking user %s print quota entry on printer %s" \
1120                                    % (self.UserName, self.PrinterName))
1121                self.Action = self.checkUserPQuota(self.UserPQuota)
1122                if self.Action.startswith("POLICY_") :
1123                    self.Action = self.Action[7:]
1124                if self.Action == "DENY" :
1125                    self.printInfo(_("Print Quota exceeded for user %s on printer %s") % (self.UserName, self.PrinterName))
1126                    self.Reason = self.config.getHardWarn(self.PrinterName)
1127                elif self.Action == "WARN" :   
1128                    self.printInfo(_("Print Quota low for user %s on printer %s") % (self.UserName, self.PrinterName))
1129                    if self.User.LimitBy and (self.User.LimitBy.lower() == "balance") : 
1130                        self.Reason = self.config.getPoorWarn()
1131                    else :     
1132                        self.Reason = self.config.getSoftWarn(self.PrinterName)
1133           
1134        # exports some new environment variables
1135        self.exportReason()
1136       
1137        # now tell the user if he needs to know something
1138        self.tellUser()
1139       
1140        # launches the pre hook
1141        self.launchPreHook()
1142       
1143        # handle starting banner pages without accounting
1144        self.BannerSize = 0
1145        accountbanner = self.config.getAccountBanner(self.PrinterName)
1146        if accountbanner in ["ENDING", "NONE"] :
1147            self.handleBanner("starting", 0)
1148       
1149        if self.Action == "DENY" :
1150            self.printInfo(_("Job denied, no accounting will be done."))
1151        elif self.Action == "CANCEL" :   
1152            self.printInfo(_("Job cancelled, no accounting will be done."))
1153        else :
1154            self.printInfo(_("Job accounting begins."))
1155            self.deinstallSigTermHandler()
1156            self.accounter.beginJob(self.Printer)
1157            self.installSigTermHandler()
1158       
1159        # handle starting banner pages with accounting
1160        if accountbanner in ["STARTING", "BOTH"] :
1161            if not self.gotSigTerm :
1162                self.handleBanner("starting", 1)
1163       
1164        # pass the job's data to the real backend   
1165        if (not self.gotSigTerm) and (self.Action in ["ALLOW", "WARN"]) :
1166            retcode = self.printJobDatas()
1167        else :       
1168            retcode = self.removeJob()
1169       
1170        # indicate phase change
1171        self.exportPhaseInfo("AFTER")
1172       
1173        # handle ending banner pages with accounting
1174        if accountbanner in ["ENDING", "BOTH"] :
1175            if not self.gotSigTerm :
1176                self.handleBanner("ending", 1)
1177       
1178        # stops accounting
1179        if self.Action == "DENY" :
1180            self.printInfo(_("Job denied, no accounting has been done."))
1181        elif self.Action == "CANCEL" :   
1182            self.printInfo(_("Job cancelled, no accounting has been done."))
1183        else :
1184            self.deinstallSigTermHandler()
1185            self.accounter.endJob(self.Printer)
1186            self.installSigTermHandler()
1187            self.printInfo(_("Job accounting ends."))
1188       
1189        # Do all these database changes within a single transaction   
1190        # NB : we don't enclose ALL the changes within a single transaction
1191        # because while waiting for the printer to answer its internal page
1192        # counter, we would open the door to accounting problems for other
1193        # jobs launched by the same user at the same time on other printers.
1194        # All the code below doesn't take much time, so it's fine.
1195        self.storage.beginTransaction()
1196        try :
1197            onbackenderror = self.config.getPrinterOnBackendError(self.PrinterName)
1198            if retcode :
1199                # NB : We don't send any feedback to the end user. Only the admin
1200                # has to know that the real CUPS backend failed.
1201                self.Action = "PROBLEM"
1202                self.exportReason()
1203                if "NOCHARGE" in onbackenderror :
1204                    self.JobSize = 0
1205                    self.printInfo(_("Job size forced to 0 because the real CUPS backend failed. No accounting will be done."), "warn")
1206                else :   
1207                    self.printInfo(_("The real CUPS backend failed, but the job will be accounted for anyway."), "warn")
1208                   
1209            # retrieve the job size   
1210            if self.Action == "DENY" :
1211                self.JobSize = 0
1212                self.printInfo(_("Job size forced to 0 because printing is denied."))
1213            elif self.Action == "CANCEL" :     
1214                self.JobSize = 0
1215                self.printInfo(_("Job size forced to 0 because printing was cancelled."))
1216            else :   
1217                self.UserPQuota.resetDenyBannerCounter()
1218                if (self.Action != "PROBLEM") or ("CHARGE" in onbackenderror) : 
1219                    self.JobSize = self.accounter.getJobSize(self.Printer)
1220                    self.sanitizeJobSize()
1221                    self.JobSize += self.BannerSize
1222            self.printInfo(_("Job size : %i") % self.JobSize)
1223           
1224            if ((self.Action == "PROBLEM") and ("NOCHARGE" in onbackenderror)) or \
1225                (self.Action in ("DENY", "CANCEL")) :
1226                self.JobPrice = 0.0
1227            elif (self.User.LimitBy == "nochange") or self.Printer.PassThrough :
1228                # no need to update the quota for the current user on this printer
1229                self.printInfo(_("User %s's quota on printer %s won't be modified") % (self.UserName, self.PrinterName))
1230                self.JobPrice = 0.0
1231            else :
1232                # update the quota for the current user on this printer
1233                self.printInfo(_("Updating user %s's quota on printer %s") % (self.UserName, self.PrinterName))
1234                self.JobPrice = self.UserPQuota.increasePagesUsage(self.JobSize)
1235           
1236            # adds the current job to history   
1237            self.Printer.addJobToHistory(self.JobId, self.User, self.accounter.getLastPageCounter(), \
1238                                    self.Action, self.JobSize, self.JobPrice, self.InputFile, \
1239                                    self.Title, self.Copies, self.Options, self.ClientHost, \
1240                                    self.JobSizeBytes, self.JobMD5Sum, None, self.JobBillingCode, \
1241                                    self.softwareJobSize, self.softwareJobPrice)
1242            self.printInfo(_("Job added to history."))
1243           
1244            if hasattr(self, "BillingCode") and self.BillingCode and self.BillingCode.Exists :
1245                if (self.Action in ("ALLOW", "WARN")) or \
1246                   ((self.Action == "PROBLEM") and ("CHARGE" in onbackenderror)) :
1247                    self.BillingCode.consume(self.JobSize, self.JobPrice)
1248                    self.printInfo(_("Billing code %s was updated.") % self.BillingCode.BillingCode)
1249        except :   
1250            self.storage.rollbackTransaction()
1251            raise
1252        else :   
1253            self.storage.commitTransaction()
1254           
1255        # exports some new environment variables
1256        self.exportJobSizeAndPrice()
1257       
1258        # then re-export user information with new values
1259        self.exportUserInfo()
1260       
1261        # handle ending banner pages without accounting
1262        if accountbanner in ["STARTING", "NONE"] :
1263            self.handleBanner("ending", 0)
1264                   
1265        self.launchPostHook()
1266           
1267        return retcode   
1268               
1269    def printJobDatas(self) :           
1270        """Sends the job's datas to the real backend."""
1271        self.logdebug("Sending job's datas to real backend...")
1272       
1273        delay = 0
1274        number = 1
1275        for onb in self.config.getPrinterOnBackendError(self.PrinterName) :
1276            if onb.startswith("RETRY:") :
1277                try :
1278                    (number, delay) = [int(p) for p in onb[6:].split(":", 2)]
1279                    if (number < 0) or (delay < 0) :
1280                        raise ValueError
1281                except ValueError :   
1282                    self.printInfo(_("Incorrect value for the 'onbackenderror' directive in section [%s]") % self.PrinterName, "error")
1283                    delay = 0
1284                    number = 1
1285                else :   
1286                    break
1287        loopcnt = 1 
1288        while True :           
1289            if self.InputFile is None :
1290                infile = open(self.DataFile, "rb")
1291            else :   
1292                infile = None
1293            retcode = self.runOriginalBackend(infile)
1294            if self.InputFile is None :
1295                infile.close()
1296            if not retcode :
1297                break
1298            else :
1299                if (not number) or (loopcnt < number) :
1300                    self.logdebug(_("The real backend produced an error, we will try again in %s seconds.") % delay)
1301                    time.sleep(delay)
1302                    loopcnt += 1
1303                else :   
1304                    break
1305           
1306        self.logdebug("Job's datas sent to real backend.")
1307        return retcode
1308       
1309    def runOriginalBackend(self, filehandle=None, isBanner=0) :
1310        """Launches the original backend."""
1311        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1312        if not isBanner :
1313            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1314        else :   
1315            # For banners, we absolutely WANT
1316            # to remove any filename from the command line !
1317            self.logdebug("It looks like we try to print a banner.")
1318            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:6]
1319        arguments[2] = self.UserName # in case it was overwritten by external script
1320        # TODO : do something about job-billing option, in case it was overwritten as well...
1321        # TODO : do something about the job title : if we are printing a banner and the backend
1322        # TODO : uses the job's title to name an output file (cups-pdf:// for example), we're stuck !
1323       
1324        self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1325        self.regainPriv()   
1326        pid = os.fork()
1327        self.logdebug("Forked !")
1328        if pid == 0 :
1329            if filehandle is not None :
1330                self.logdebug("Redirecting file handle to real backend's stdin")
1331                os.dup2(filehandle.fileno(), 0)
1332            try :
1333                self.logdebug("Calling execve...")
1334                os.execve(originalbackend, arguments, os.environ)
1335            except OSError, msg :
1336                self.logdebug("execve() failed: %s" % msg)
1337            self.logdebug("We shouldn't be there !!!")   
1338            os._exit(-1)
1339        self.dropPriv()   
1340       
1341        self.logdebug("Waiting for original backend to exit...")   
1342        killed = 0
1343        status = -1
1344        while status == -1 :
1345            try :
1346                status = os.waitpid(pid, 0)[1]
1347            except OSError, (err, msg) :
1348                if (err == 4) and self.gotSigTerm :
1349                    os.kill(pid, signal.SIGTERM)
1350                    killed = 1
1351                   
1352        if os.WIFEXITED(status) :
1353            status = os.WEXITSTATUS(status)
1354            message = "CUPS backend %s returned %d." % \
1355                            (originalbackend, status)
1356            if status :
1357                level = "error"
1358                self.Reason = message
1359            else :   
1360                level = "info"
1361            self.printInfo(message, level)
1362            return status
1363        elif not killed :
1364            self.Reason = "CUPS backend %s died abnormally." % originalbackend
1365            self.printInfo(self.Reason, "error")
1366            return -1
1367        else :
1368            self.Reason = "CUPS backend %s was killed." % originalbackend
1369            self.printInfo(self.Reason, "warn")
1370            return 1
1371       
1372if __name__ == "__main__" :   
1373    # This is a CUPS backend, we should act and die like a CUPS backend
1374    wrapper = CUPSBackend()
1375    if len(sys.argv) == 1 :
1376        print "\n".join(wrapper.discoverOtherBackends())
1377        sys.exit(0)               
1378    elif len(sys.argv) not in (6, 7) :   
1379        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1380                              % sys.argv[0])
1381        sys.exit(1)
1382    else :   
1383        try :
1384            wrapper.deferredInit()
1385            wrapper.initBackendParameters()
1386            wrapper.waitForLock()
1387            wrapper.saveDatasAndCheckSum()
1388            wrapper.preaccounter = openAccounter(wrapper, ispreaccounter=1)
1389            wrapper.accounter = openAccounter(wrapper)
1390            wrapper.precomputeJobSize()
1391            wrapper.exportJobInfo() # exports a first time to give hints to external scripts
1392            wrapper.overwriteJobAttributes()
1393            wrapper.exportJobInfo() # re-exports in case it was overwritten
1394            retcode = wrapper.mainWork()
1395        except KeyboardInterrupt :   
1396            wrapper.printInfo(_("Job %s interrupted by the administrator !") % wrapper.JobId, "warn")
1397            retcode = 0
1398        except SystemExit, err :   
1399            retcode = err.code
1400        except :   
1401            try :
1402                wrapper.crashed("cupspykota backend failed")
1403            except :   
1404                crashed("cupspykota backend failed")
1405            retcode = 1
1406        wrapper.clean()
1407        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.