root / pykota / trunk / bin / cupspykota @ 3036

Revision 3036, 69.9 kB (checked in by jerome, 18 years ago)

Charging for ink usage, finally !

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