root / pykota / trunk / bin / cupspykota @ 3158

Revision 3158, 71.8 kB (checked in by matt, 17 years ago)

Allow configuration to avoid printing banner pages for consecutive jobs for the same user on the same printer

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