root / pykota / trunk / bin / cupspykota @ 3260

Revision 3260, 68.0 kB (checked in by jerome, 16 years ago)

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

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