root / pykota / trunk / bin / cupspykota @ 3059

Revision 3059, 69.0 kB (checked in by jerome, 17 years ago)

Now logs where print job's datas come from.

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