root / pykota / trunk / bin / cupspykota @ 3171

Revision 3171, 72.5 kB (checked in by jerome, 17 years ago)

Still apply the deprecated utolower directive if it is set, in order
to not break existing configurations.

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