root / pykota / trunk / bin / cupspykota @ 3146

Revision 3146, 69.9 kB (checked in by matt, 17 years ago)

Allow overwrite_jobticket scripts to output a REASON= line to change the default reason a job was denied or cancelled.

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