root / pykota / branches / 1.26_fixes / bin / cupspykota @ 3462

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