root / pykota / trunk / bin / cupspykota @ 3070

Revision 3070, 69.2 kB (checked in by jerome, 17 years ago)

Fixed problem with hplip's device_uris

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