root / pykota / trunk / bin / cupspykota @ 3276

Revision 3276, 66.9 kB (checked in by jerome, 16 years ago)

Doesn't drop and regain priviledges anymore : no added security since we could regain them (we needed to regain them for PAM and some end user scripts). This is also more consistent.
Removed SGTERM handling stuff in cupspykota : now only SIGINT can be used.
Now outputs an error message when printing (but doesn't fail) if CUPS is
not v1.3.4 or higher : we need 1.3.4 or higher because it fixes some
problematic charset handling bugs (by only accepting ascii and utf-8,
but this is a different story...)
Now ensures only the supported exit codes are returned by cupspykota :
we used to exit -1 in some cases (see man backend for details).

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