root / pykota / trunk / bin / cupspykota @ 3463

Revision 3463, 65.8 kB (checked in by jerome, 15 years ago)

Fixed an uninitialized variable problem.

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