root / pykota / trunk / bin / cupspykota @ 3153

Revision 3153, 70.0 kB (checked in by jerome, 17 years ago)

Don't ask for confirmation anymore if there's not enough credits
to print.

  • 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 self.Printer.MaxJobSize and (self.softwareJobSize > self.Printer.MaxJobSize) :
1108                # This printer was set to refuse jobs this large.
1109                self.printInfo(_("Precomputed job size (%s pages) too large for printer %s.") % (self.softwareJobSize, self.PrinterName), "warn")
1110                self.Action = "DENY"
1111                # here we don't put the precomputed job size in the message
1112                # because in case of error the user could complain :-)
1113                self.Reason = _("You are not allowed to print so many pages on printer %s at this time.") % self.PrinterName
1114           
1115        if self.Action not in ("DENY", "CANCEL") :
1116            if self.User.LimitBy == "noprint" :
1117                self.printInfo(_("User %s is not allowed to print at this time.") % self.UserName, "warn")
1118                self.Action = "DENY"
1119                self.Reason = _("Your account settings forbid you to print at this time.")
1120               
1121        if self.Action not in ("DENY", "CANCEL") :
1122            # If printing is still allowed at this time, we
1123            # need to extract the billing code information from the database.
1124            # No need to do this if the job is denied, this way we
1125            # save some database queries.
1126            self.getBillingCode()
1127           
1128        if self.Action not in ("DENY", "CANCEL") :
1129            # If printing is still allowed at this time, we
1130            # need to check if the job is a dupe or not, and what to do then.
1131            # No need to do this if the job is denied, this way we
1132            # save some database queries.
1133            self.checkIfDupe()
1134                   
1135        if self.Action not in ("DENY", "CANCEL") :
1136            # If printing is still allowed at this time, we
1137            # need to check the user's print quota on the current printer.
1138            # No need to do this if the job is denied, this way we
1139            # save some database queries.
1140            if self.User.LimitBy in ('noquota', 'nochange') :
1141                self.logdebug("User %s is allowed to print with no limit, no need to check quota." % self.UserName)
1142            elif self.Printer.PassThrough :   
1143                self.logdebug("Printer %s is in PassThrough mode, no need to check quota." % self.PrinterName)
1144            else :
1145                self.logdebug("Checking user %s print quota entry on printer %s" \
1146                                    % (self.UserName, self.PrinterName))
1147                self.Action = self.checkUserPQuota(self.UserPQuota)
1148                if self.Action.startswith("POLICY_") :
1149                    self.Action = self.Action[7:]
1150                if self.Action == "DENY" :
1151                    self.printInfo(_("Print Quota exceeded for user %s on printer %s") % (self.UserName, self.PrinterName))
1152                    self.Reason = self.config.getHardWarn(self.PrinterName)
1153                elif self.Action == "WARN" :   
1154                    self.printInfo(_("Print Quota low for user %s on printer %s") % (self.UserName, self.PrinterName))
1155                    if self.User.LimitBy and (self.User.LimitBy.lower() == "balance") : 
1156                        self.Reason = self.config.getPoorWarn()
1157                    else :     
1158                        self.Reason = self.config.getSoftWarn(self.PrinterName)
1159           
1160        # If job still allowed to print, should we ask for confirmation ?   
1161        if self.Action not in ("DENY", "CANCEL") : 
1162            if not self.didUserConfirm() :
1163                self.Action = "CANCEL"
1164                self.Reason = _("Print job cancelled.")
1165                os.environ["PYKOTASTATUS"] = "CANCELLED"
1166               
1167        # exports some new environment variables
1168        self.exportReason()
1169       
1170        # now tell the user if he needs to know something
1171        self.tellUser()
1172       
1173        # launches the pre hook
1174        self.launchPreHook()
1175       
1176        # handle starting banner pages without accounting
1177        self.BannerSize = 0
1178        accountbanner = self.config.getAccountBanner(self.PrinterName)
1179        if (self.Action != "CANCEL") and accountbanner in ["ENDING", "NONE"] :
1180            self.handleBanner("starting", 0)
1181       
1182        if self.Action == "DENY" :
1183            self.printInfo(_("Job denied, no accounting will be done."))
1184        elif self.Action == "CANCEL" :   
1185            self.printInfo(_("Job cancelled, no accounting will be done."))
1186        else :
1187            self.printInfo(_("Job accounting begins."))
1188            self.deinstallSigTermHandler()
1189            self.accounter.beginJob(self.Printer)
1190            self.installSigTermHandler()
1191       
1192        # handle starting banner pages with accounting
1193        if (self.Action != "CANCEL") and accountbanner in ["STARTING", "BOTH"] :
1194            if not self.gotSigTerm :
1195                self.handleBanner("starting", 1)
1196       
1197        # pass the job's data to the real backend   
1198        if (not self.gotSigTerm) and (self.Action in ["ALLOW", "WARN"]) :
1199            retcode = self.printJobDatas()
1200        else :       
1201            retcode = self.removeJob()
1202       
1203        # indicate phase change
1204        self.exportPhaseInfo("AFTER")
1205       
1206        # handle ending banner pages with accounting
1207        if (self.Action != "CANCEL") and accountbanner in ["ENDING", "BOTH"] :
1208            if not self.gotSigTerm :
1209                self.handleBanner("ending", 1)
1210       
1211        # stops accounting
1212        if self.Action == "DENY" :
1213            self.printInfo(_("Job denied, no accounting has been done."))
1214        elif self.Action == "CANCEL" :   
1215            self.printInfo(_("Job cancelled, no accounting has been done."))
1216        else :
1217            self.deinstallSigTermHandler()
1218            self.accounter.endJob(self.Printer)
1219            self.installSigTermHandler()
1220            self.printInfo(_("Job accounting ends."))
1221       
1222        # Do all these database changes within a single transaction   
1223        # NB : we don't enclose ALL the changes within a single transaction
1224        # because while waiting for the printer to answer its internal page
1225        # counter, we would open the door to accounting problems for other
1226        # jobs launched by the same user at the same time on other printers.
1227        # All the code below doesn't take much time, so it's fine.
1228        self.storage.beginTransaction()
1229        try :
1230            onbackenderror = self.config.getPrinterOnBackendError(self.PrinterName)
1231            if retcode :
1232                # NB : We don't send any feedback to the end user. Only the admin
1233                # has to know that the real CUPS backend failed.
1234                self.Action = "PROBLEM"
1235                self.exportReason()
1236                if "NOCHARGE" in onbackenderror :
1237                    self.JobSize = 0
1238                    self.printInfo(_("Job size forced to 0 because the real CUPS backend failed. No accounting will be done."), "warn")
1239                else :   
1240                    self.printInfo(_("The real CUPS backend failed, but the job will be accounted for anyway."), "warn")
1241                   
1242            # retrieve the job size   
1243            if self.Action == "DENY" :
1244                self.JobSize = 0
1245                self.printInfo(_("Job size forced to 0 because printing is denied."))
1246            elif self.Action == "CANCEL" :     
1247                self.JobSize = 0
1248                self.printInfo(_("Job size forced to 0 because printing was cancelled."))
1249            else :   
1250                self.UserPQuota.resetDenyBannerCounter()
1251                if (self.Action != "PROBLEM") or ("CHARGE" in onbackenderror) : 
1252                    self.JobSize = self.accounter.getJobSize(self.Printer)
1253                    self.sanitizeJobSize()
1254                    self.JobSize += self.BannerSize
1255            self.printInfo(_("Job size : %i") % self.JobSize)
1256           
1257            if ((self.Action == "PROBLEM") and ("NOCHARGE" in onbackenderror)) or \
1258                (self.Action in ("DENY", "CANCEL")) :
1259                self.JobPrice = 0.0
1260            elif (self.User.LimitBy == "nochange") or self.Printer.PassThrough :
1261                # no need to update the quota for the current user on this printer
1262                self.printInfo(_("User %s's quota on printer %s won't be modified") % (self.UserName, self.PrinterName))
1263                self.JobPrice = 0.0
1264            else :
1265                # update the quota for the current user on this printer
1266                self.printInfo(_("Updating user %s's quota on printer %s") % (self.UserName, self.PrinterName))
1267                self.JobPrice = self.UserPQuota.increasePagesUsage(self.JobSize, self.accounter.inkUsage)
1268           
1269            # adds the current job to history   
1270            self.Printer.addJobToHistory(self.JobId, self.User, self.accounter.getLastPageCounter(), \
1271                                    self.Action, self.JobSize, self.JobPrice, self.InputFile, \
1272                                    self.Title, self.Copies, self.Options, self.ClientHost, \
1273                                    self.JobSizeBytes, self.JobMD5Sum, None, self.JobBillingCode, \
1274                                    self.softwareJobSize, self.softwareJobPrice)
1275            self.printInfo(_("Job added to history."))
1276           
1277            if hasattr(self, "BillingCode") and self.BillingCode and self.BillingCode.Exists :
1278                if (self.Action in ("ALLOW", "WARN")) or \
1279                   ((self.Action == "PROBLEM") and ("CHARGE" in onbackenderror)) :
1280                    self.BillingCode.consume(self.JobSize, self.JobPrice)
1281                    self.printInfo(_("Billing code %s was updated.") % self.BillingCode.BillingCode)
1282        except :   
1283            self.storage.rollbackTransaction()
1284            raise
1285        else :   
1286            self.storage.commitTransaction()
1287           
1288        # exports some new environment variables
1289        self.exportJobSizeAndPrice()
1290       
1291        # then re-export user information with new values
1292        self.exportUserInfo()
1293       
1294        # handle ending banner pages without accounting
1295        if (self.Action != "CANCEL") and accountbanner in ["STARTING", "NONE"] :
1296            self.handleBanner("ending", 0)
1297                   
1298        self.launchPostHook()
1299           
1300        return retcode   
1301               
1302    def printJobDatas(self) :           
1303        """Sends the job's datas to the real backend."""
1304        self.logdebug("Sending job's datas to real backend...")
1305       
1306        delay = 0
1307        number = 1
1308        for onb in self.config.getPrinterOnBackendError(self.PrinterName) :
1309            if onb.startswith("RETRY:") :
1310                try :
1311                    (number, delay) = [int(p) for p in onb[6:].split(":", 2)]
1312                    if (number < 0) or (delay < 0) :
1313                        raise ValueError
1314                except ValueError :   
1315                    self.printInfo(_("Incorrect value for the 'onbackenderror' directive in section [%s]") % self.PrinterName, "error")
1316                    delay = 0
1317                    number = 1
1318                else :   
1319                    break
1320        loopcnt = 1 
1321        while True :           
1322            if self.InputFile is None :
1323                infile = open(self.DataFile, "rb")
1324            else :   
1325                infile = None
1326            retcode = self.runOriginalBackend(infile)
1327            if self.InputFile is None :
1328                infile.close()
1329            if not retcode :
1330                break
1331            else :
1332                if (not number) or (loopcnt < number) :
1333                    self.logdebug(_("The real backend produced an error, we will try again in %s seconds.") % delay)
1334                    time.sleep(delay)
1335                    loopcnt += 1
1336                else :   
1337                    break
1338           
1339        self.logdebug("Job's datas sent to real backend.")
1340        return retcode
1341       
1342    def runOriginalBackend(self, filehandle=None, isBanner=0) :
1343        """Launches the original backend."""
1344        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1345        if not isBanner :
1346            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1347        else :   
1348            # For banners, we absolutely WANT
1349            # to remove any filename from the command line !
1350            self.logdebug("It looks like we try to print a banner.")
1351            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:6]
1352        arguments[2] = self.UserName # in case it was overwritten by external script
1353        # TODO : do something about job-billing option, in case it was overwritten as well...
1354        # TODO : do something about the job title : if we are printing a banner and the backend
1355        # TODO : uses the job's title to name an output file (cups-pdf:// for example), we're stuck !
1356       
1357        self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1358        self.regainPriv()   
1359        pid = os.fork()
1360        self.logdebug("Forked !")
1361        if pid == 0 :
1362            if filehandle is not None :
1363                self.logdebug("Redirecting file handle to real backend's stdin")
1364                os.dup2(filehandle.fileno(), 0)
1365            try :
1366                self.logdebug("Calling execve...")
1367                os.execve(originalbackend, arguments, os.environ)
1368            except OSError, msg :
1369                self.logdebug("execve() failed: %s" % msg)
1370            self.logdebug("We shouldn't be there !!!")   
1371            os._exit(-1)
1372        self.dropPriv()   
1373       
1374        self.logdebug("Waiting for original backend to exit...")   
1375        killed = 0
1376        status = -1
1377        while status == -1 :
1378            try :
1379                status = os.waitpid(pid, 0)[1]
1380            except OSError, (err, msg) :
1381                if (err == 4) and self.gotSigTerm :
1382                    os.kill(pid, signal.SIGTERM)
1383                    killed = 1
1384                   
1385        if os.WIFEXITED(status) :
1386            status = os.WEXITSTATUS(status)
1387            message = "CUPS backend %s returned %d." % \
1388                            (originalbackend, status)
1389            if status :
1390                level = "error"
1391                self.Reason = message
1392            else :   
1393                level = "info"
1394            self.printInfo(message, level)
1395            return status
1396        elif not killed :
1397            self.Reason = "CUPS backend %s died abnormally." % originalbackend
1398            self.printInfo(self.Reason, "error")
1399            return -1
1400        else :
1401            self.Reason = "CUPS backend %s was killed." % originalbackend
1402            self.printInfo(self.Reason, "warn")
1403            return 1
1404       
1405if __name__ == "__main__" :   
1406    # This is a CUPS backend, we should act and die like a CUPS backend
1407    wrapper = CUPSBackend()
1408    if len(sys.argv) == 1 :
1409        print "\n".join(wrapper.discoverOtherBackends())
1410        sys.exit(0)               
1411    elif len(sys.argv) not in (6, 7) :   
1412        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1413                              % sys.argv[0])
1414        sys.exit(1)
1415    else :   
1416        os.environ["PATH"] = "%s:/bin:/usr/bin:/usr/local/bin:/opt/bin:/sbin:/usr/sbin" % os.environ.get("PATH", "")
1417        try :
1418            try :
1419                wrapper.deferredInit()
1420                wrapper.initBackendParameters()
1421                wrapper.waitForLock()
1422                if os.environ.get("PYKOTASTATUS") == "CANCELLED" :
1423                    raise KeyboardInterrupt
1424                wrapper.saveDatasAndCheckSum()
1425                wrapper.preaccounter = openAccounter(wrapper, ispreaccounter=1)
1426                wrapper.accounter = openAccounter(wrapper)
1427                wrapper.precomputeJobSize()
1428                wrapper.exportJobInfo() # exports a first time to give hints to external scripts
1429                wrapper.overwriteJobAttributes()
1430                wrapper.exportJobInfo() # re-exports in case it was overwritten
1431                retcode = wrapper.mainWork()
1432            except KeyboardInterrupt :   
1433                wrapper.printInfo(_("Job %s interrupted by the administrator !") % wrapper.JobId, "warn")
1434                retcode = 0
1435            except SystemExit, err :   
1436                retcode = err.code
1437            except :   
1438                try :
1439                    wrapper.crashed("cupspykota backend failed")
1440                except :   
1441                    crashed("cupspykota backend failed")
1442                retcode = 1
1443        finally :       
1444            wrapper.clean()
1445        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.