root / pykota / trunk / bin / cupspykota @ 3169

Revision 3169, 72.2 kB (checked in by jerome, 17 years ago)

Finish implementation of the usernamecase directive.

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