root / pykota / trunk / bin / cupspykota @ 3049

Revision 3049, 68.9 kB (checked in by jerome, 17 years ago)

Removed unneeded code.

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