root / pykota / trunk / bin / cupspykota @ 2932

Revision 2929, 66.7 kB (checked in by jerome, 18 years ago)

Fixed a problem with the overwriting of the job's billing code.

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