root / pykota / trunk / bin / cupspykota @ 2997

Revision 2997, 67.6 kB (checked in by jerome, 18 years ago)

Avoids eating all filenums :-)

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