root / pykota / trunk / pykota / tool.py @ 1584

Revision 1584, 50.2 kB (checked in by jalet, 20 years ago)

Better dispatching of error messages

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 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 2 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, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23# $Log$
24# Revision 1.110  2004/07/01 19:56:42  jalet
25# Better dispatching of error messages
26#
27# Revision 1.109  2004/07/01 17:45:49  jalet
28# Added code to handle the description field for printers
29#
30# Revision 1.108  2004/06/24 23:09:30  jalet
31# Also prints read size on last block
32#
33# Revision 1.107  2004/06/23 13:03:28  jalet
34# Catches accounter configuration errors earlier
35#
36# Revision 1.106  2004/06/22 09:31:18  jalet
37# Always send some debug info to CUPS' back channel stream (stderr) as
38# informationnal messages.
39#
40# Revision 1.105  2004/06/21 08:17:38  jalet
41# Added version number in subject message for directive crashrecipient.
42#
43# Revision 1.104  2004/06/18 13:34:49  jalet
44# Now all tracebacks include PyKota's version number
45#
46# Revision 1.103  2004/06/18 13:17:26  jalet
47# Now includes PyKota's version number in messages sent by the crashrecipient
48# directive.
49#
50# Revision 1.102  2004/06/17 13:26:51  jalet
51# Better exception handling code
52#
53# Revision 1.101  2004/06/16 20:56:34  jalet
54# Smarter initialisation code
55#
56# Revision 1.100  2004/06/11 08:16:03  jalet
57# More exceptions catched in case of very early failure.
58#
59# Revision 1.99  2004/06/11 07:07:38  jalet
60# Now detects and logs configuration syntax errors instead of failing without
61# any notice message.
62#
63# Revision 1.98  2004/06/08 19:27:12  jalet
64# Doesn't ignore SIGCHLD anymore
65#
66# Revision 1.97  2004/06/07 22:45:35  jalet
67# Now accepts a job when enforcement is STRICT and predicted account balance
68# is equal to 0.0 : since the job hasn't been printed yet, only its printing
69# will really render balance equal to 0.0, so we should be allowed to print.
70#
71# Revision 1.96  2004/06/05 22:18:04  jalet
72# Now catches some exceptions earlier.
73# storage.py and ldapstorage.py : removed old comments
74#
75# Revision 1.95  2004/06/03 21:50:34  jalet
76# Improved error logging.
77# crashrecipient directive added.
78# Now exports the job's size in bytes too.
79#
80# Revision 1.94  2004/06/03 08:51:03  jalet
81# logs job's size in bytes now
82#
83# Revision 1.93  2004/06/02 21:51:02  jalet
84# Moved the sigterm capturing elsewhere
85#
86# Revision 1.92  2004/06/02 13:21:38  jalet
87# Debug message added
88#
89# Revision 1.91  2004/05/25 05:17:52  jalet
90# Now precomputes the job's size only if current printer's enforcement
91# is "STRICT"
92#
93# Revision 1.90  2004/05/24 22:45:49  jalet
94# New 'enforcement' directive added
95# Polling loop improvements
96#
97# Revision 1.89  2004/05/21 22:02:52  jalet
98# Preliminary work on pre-accounting
99#
100# Revision 1.88  2004/05/18 14:49:20  jalet
101# Big code changes to completely remove the need for "requester" directives,
102# jsut use "hardware(... your previous requester directive's content ...)"
103#
104# Revision 1.87  2004/05/17 19:14:59  jalet
105# Now catches SIGPIPE and SIGCHLD
106#
107# Revision 1.86  2004/05/13 13:59:28  jalet
108# Code simplifications
109#
110# Revision 1.85  2004/05/11 08:26:27  jalet
111# Now catches connection problems to SMTP server
112#
113# Revision 1.84  2004/04/21 08:36:32  jalet
114# Exports the PYKOTASTATUS environment variable when SIGTERM is received.
115#
116# Revision 1.83  2004/04/16 17:03:49  jalet
117# The list of printers groups the current printer is a member of is
118# now exported in the PYKOTAPGROUPS environment variable
119#
120# Revision 1.82  2004/04/13 09:38:03  jalet
121# More work on correct child processes handling
122#
123# Revision 1.81  2004/04/09 22:24:47  jalet
124# Began work on correct handling of child processes when jobs are cancelled by
125# the user. Especially important when an external requester is running for a
126# long time.
127#
128# Revision 1.80  2004/04/06 12:00:21  jalet
129# uninitialized values caused problems
130#
131# Revision 1.79  2004/03/28 21:01:29  jalet
132# PYKOTALIMITBY environment variable is now exported too
133#
134# Revision 1.78  2004/03/08 20:13:25  jalet
135# Allow names to begin with a digit
136#
137# Revision 1.77  2004/03/03 13:10:35  jalet
138# Now catches all smtplib exceptions when there's a problem sending messages
139#
140# Revision 1.76  2004/03/01 14:34:15  jalet
141# PYKOTAPHASE wasn't set at the right time at the end of data transmission
142# to underlying layer (real backend)
143#
144# Revision 1.75  2004/03/01 11:23:25  jalet
145# Pre and Post hooks to external commands are available in the cupspykota
146# backend. Forthe pykota filter they will be implemented real soon now.
147#
148# Revision 1.74  2004/02/26 14:18:07  jalet
149# Should fix the remaining bugs wrt printers groups and users groups.
150#
151# Revision 1.73  2004/02/19 14:20:21  jalet
152# maildomain pykota.conf directive added.
153# Small improvements on mail headers quality.
154#
155# Revision 1.72  2004/01/14 15:51:19  jalet
156# Docstring added.
157#
158# Revision 1.71  2004/01/11 23:22:42  jalet
159# Major code refactoring, it's way cleaner, and now allows automated addition
160# of printers on first print.
161#
162# Revision 1.70  2004/01/08 14:10:32  jalet
163# Copyright year changed.
164#
165# Revision 1.69  2004/01/05 16:02:18  jalet
166# Dots in user, groups and printer names should be allowed.
167#
168# Revision 1.68  2004/01/02 17:38:40  jalet
169# This time it should be better...
170#
171# Revision 1.67  2004/01/02 17:37:09  jalet
172# I'm completely stupid !!! Better to not talk while coding !
173#
174# Revision 1.66  2004/01/02 17:31:26  jalet
175# Forgot to remove some code
176#
177# Revision 1.65  2003/12/06 08:14:38  jalet
178# Added support for CUPS device uris which contain authentication information.
179#
180# Revision 1.64  2003/11/29 22:03:17  jalet
181# Some code refactoring work. New code is not used at this time.
182#
183# Revision 1.63  2003/11/29 20:06:20  jalet
184# Added 'utolower' configuration option to convert all usernames to
185# lowercase when printing. All database accesses are still and will
186# remain case sensitive though.
187#
188# Revision 1.62  2003/11/25 22:37:22  jalet
189# Small code move
190#
191# Revision 1.61  2003/11/25 22:03:28  jalet
192# No more message on stderr when the translation is not available.
193#
194# Revision 1.60  2003/11/25 21:54:05  jalet
195# updated FAQ
196#
197# Revision 1.59  2003/11/25 13:33:43  jalet
198# Puts 'root' instead of '' when printing from CUPS web interface (which
199# gives an empty username)
200#
201# Revision 1.58  2003/11/21 14:28:45  jalet
202# More complete job history.
203#
204# Revision 1.57  2003/11/19 23:19:38  jalet
205# Code refactoring work.
206# Explicit redirection to /dev/null has to be set in external policy now, just
207# like in external mailto.
208#
209# Revision 1.56  2003/11/19 07:40:20  jalet
210# Missing import statement.
211# Better documentation for mailto: external(...)
212#
213# Revision 1.55  2003/11/18 23:43:12  jalet
214# Mailto can be any external command now, as usual.
215#
216# Revision 1.54  2003/10/24 21:52:46  jalet
217# Now can force language when coming from CGI script.
218#
219# Revision 1.53  2003/10/08 21:41:38  jalet
220# External policies for printers works !
221# We can now auto-add users on first print, and do other useful things if needed.
222#
223# Revision 1.52  2003/10/07 09:07:28  jalet
224# Character encoding added to please latest version of Python
225#
226# Revision 1.51  2003/10/06 14:21:41  jalet
227# Test reversed to not retrieve group members when no messages for them.
228#
229# Revision 1.50  2003/10/02 20:23:18  jalet
230# Storage caching mechanism added.
231#
232# Revision 1.49  2003/07/29 20:55:17  jalet
233# 1.14 is out !
234#
235# Revision 1.48  2003/07/21 23:01:56  jalet
236# Modified some messages aout soft limit
237#
238# Revision 1.47  2003/07/16 21:53:08  jalet
239# Really big modifications wrt new configuration file's location and content.
240#
241# Revision 1.46  2003/07/09 20:17:07  jalet
242# Email field added to PostgreSQL schema
243#
244# Revision 1.45  2003/07/08 19:43:51  jalet
245# Configurable warning messages.
246# Poor man's treshold value added.
247#
248# Revision 1.44  2003/07/07 11:49:24  jalet
249# Lots of small fixes with the help of PyChecker
250#
251# Revision 1.43  2003/07/04 09:06:32  jalet
252# Small bug fix wrt undefined "LimitBy" field.
253#
254# Revision 1.42  2003/06/30 12:46:15  jalet
255# Extracted reporting code.
256#
257# Revision 1.41  2003/06/25 14:10:01  jalet
258# Hey, it may work (edpykota --reset excepted) !
259#
260# Revision 1.40  2003/06/10 16:37:54  jalet
261# Deletion of the second user which is not needed anymore.
262# Added a debug configuration field in /etc/pykota.conf
263# All queries can now be sent to the logger in debug mode, this will
264# greatly help improve performance when time for this will come.
265#
266# Revision 1.39  2003/04/29 18:37:54  jalet
267# Pluggable accounting methods (actually doesn't support external scripts)
268#
269# Revision 1.38  2003/04/24 11:53:48  jalet
270# Default policy for unknown users/groups is to DENY printing instead
271# of the previous default to ALLOW printing. This is to solve an accuracy
272# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
273# (from PyKota's POV) will be charged to the next user who prints on the
274# same printer.
275#
276# Revision 1.37  2003/04/24 08:08:27  jalet
277# Debug message forgotten
278#
279# Revision 1.36  2003/04/24 07:59:40  jalet
280# LPRng support now works !
281#
282# Revision 1.35  2003/04/23 22:13:57  jalet
283# Preliminary support for LPRng added BUT STILL UNTESTED.
284#
285# Revision 1.34  2003/04/17 09:26:21  jalet
286# repykota now reports account balances too.
287#
288# Revision 1.33  2003/04/16 12:35:49  jalet
289# Groups quota work now !
290#
291# Revision 1.32  2003/04/16 08:53:14  jalet
292# Printing can now be limited either by user's account balance or by
293# page quota (the default). Quota report doesn't include account balance
294# yet, though.
295#
296# Revision 1.31  2003/04/15 11:30:57  jalet
297# More work done on money print charging.
298# Minor bugs corrected.
299# All tools now access to the storage as priviledged users, repykota excepted.
300#
301# Revision 1.30  2003/04/10 21:47:20  jalet
302# Job history added. Upgrade script neutralized for now !
303#
304# Revision 1.29  2003/03/29 13:45:27  jalet
305# GPL paragraphs were incorrectly (from memory) copied into the sources.
306# Two README files were added.
307# Upgrade script for PostgreSQL pre 1.01 schema was added.
308#
309# Revision 1.28  2003/03/29 13:08:28  jalet
310# Configuration is now expected to be found in /etc/pykota.conf instead of
311# in /etc/cups/pykota.conf
312# Installation script can move old config files to the new location if needed.
313# Better error handling if configuration file is absent.
314#
315# Revision 1.27  2003/03/15 23:01:28  jalet
316# New mailto option in configuration file added.
317# No time to test this tonight (although it should work).
318#
319# Revision 1.26  2003/03/09 23:58:16  jalet
320# Comment
321#
322# Revision 1.25  2003/03/07 22:56:14  jalet
323# 0.99 is out with some bug fixes.
324#
325# Revision 1.24  2003/02/27 23:48:41  jalet
326# Correctly maps PyKota's log levels to syslog log levels
327#
328# Revision 1.23  2003/02/27 22:55:20  jalet
329# WARN log priority doesn't exist.
330#
331# Revision 1.22  2003/02/27 09:09:20  jalet
332# Added a method to match strings against wildcard patterns
333#
334# Revision 1.21  2003/02/17 23:01:56  jalet
335# Typos
336#
337# Revision 1.20  2003/02/17 22:55:01  jalet
338# More options can now be set per printer or globally :
339#
340#       admin
341#       adminmail
342#       gracedelay
343#       requester
344#
345# the printer option has priority when both are defined.
346#
347# Revision 1.19  2003/02/10 11:28:45  jalet
348# Localization
349#
350# Revision 1.18  2003/02/10 01:02:17  jalet
351# External requester is about to work, but I must sleep
352#
353# Revision 1.17  2003/02/09 13:05:43  jalet
354# Internationalization continues...
355#
356# Revision 1.16  2003/02/09 12:56:53  jalet
357# Internationalization begins...
358#
359# Revision 1.15  2003/02/08 22:09:52  jalet
360# Name check method moved here
361#
362# Revision 1.14  2003/02/07 10:42:45  jalet
363# Indentation problem
364#
365# Revision 1.13  2003/02/07 08:34:16  jalet
366# Test wrt date limit was wrong
367#
368# Revision 1.12  2003/02/06 23:20:02  jalet
369# warnpykota doesn't need any user/group name argument, mimicing the
370# warnquota disk quota tool.
371#
372# Revision 1.11  2003/02/06 22:54:33  jalet
373# warnpykota should be ok
374#
375# Revision 1.10  2003/02/06 15:03:11  jalet
376# added a method to set the limit date
377#
378# Revision 1.9  2003/02/06 10:39:23  jalet
379# Preliminary edpykota work.
380#
381# Revision 1.8  2003/02/06 09:19:02  jalet
382# More robust behavior (hopefully) when the user or printer is not managed
383# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
384# printer and/or user not 'yet?' in storage.
385#
386# Revision 1.7  2003/02/06 00:00:45  jalet
387# Now includes the printer name in email messages
388#
389# Revision 1.6  2003/02/05 23:55:02  jalet
390# Cleaner email messages
391#
392# Revision 1.5  2003/02/05 23:45:09  jalet
393# Better DateTime manipulation wrt grace delay
394#
395# Revision 1.4  2003/02/05 23:26:22  jalet
396# Incorrect handling of grace delay
397#
398# Revision 1.3  2003/02/05 22:16:20  jalet
399# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
400#
401# Revision 1.2  2003/02/05 22:10:29  jalet
402# Typos
403#
404# Revision 1.1  2003/02/05 21:28:17  jalet
405# Initial import into CVS
406#
407#
408#
409
410import sys
411import os
412import fnmatch
413import getopt
414import smtplib
415import gettext
416import locale
417import signal
418import socket
419import tempfile
420import ConfigParser
421
422from mx import DateTime
423
424from pykota import version, config, storage, logger, accounter, pdlanalyzer
425
426class PyKotaToolError(Exception):
427    """An exception for PyKota config related stuff."""
428    def __init__(self, message = ""):
429        self.message = message
430        Exception.__init__(self, message)
431    def __repr__(self):
432        return self.message
433    __str__ = __repr__
434   
435def crashed(message) :   
436    """Minimal crash method."""
437    import traceback
438    lines = []
439    for line in traceback.format_exception(*sys.exc_info()) :
440        lines.extend([l for l in line.split("\n") if l])
441    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % version.__version__, message] + lines)])
442    sys.stderr.write(msg)
443    sys.stderr.flush()
444    return msg
445
446class PyKotaTool :   
447    """Base class for all PyKota command line tools."""
448    def __init__(self, lang=None, doc="PyKota %s (c) 2003-2004 %s" % (version.__version__, version.__author__)) :
449        """Initializes the command line tool."""
450        # locale stuff
451        try :
452            locale.setlocale(locale.LC_ALL, lang)
453            gettext.install("pykota")
454        except (locale.Error, IOError) :
455            gettext.NullTranslations().install()
456   
457        # pykota specific stuff
458        self.documentation = doc
459        try :
460            self.config = config.PyKotaConfig("/etc/pykota")
461        except ConfigParser.ParsingError, msg :   
462            sys.stderr.write("ERROR: Problem encountered while parsing configuration file : %s\n" % msg)
463            sys.stderr.flush()
464            sys.exit(-1)
465           
466        try :
467            self.debug = self.config.getDebug()
468            self.smtpserver = self.config.getSMTPServer()
469            self.maildomain = self.config.getMailDomain()
470            self.logger = logger.openLogger(self.config.getLoggingBackend())
471            self.storage = storage.openConnection(self)
472        except (config.PyKotaConfigError, logger.PyKotaLoggingError, storage.PyKotaStorageError), msg :
473            self.crashed(msg)
474            raise
475        self.softwareJobSize = 0
476        self.softwareJobPrice = 0.0
477       
478    def logdebug(self, message) :   
479        """Logs something to debug output if debug is enabled."""
480        if self.debug :
481            self.logger.log_message(message, "debug")
482           
483    def printInfo(self, message, level="info") :       
484        """Sends a message to standard error."""
485        sys.stderr.write("%s: %s\n" % (level.upper(), message))
486        sys.stderr.flush()
487       
488    def clean(self) :   
489        """Ensures that the database is closed."""
490        try :
491            self.storage.close()
492        except (TypeError, NameError, AttributeError) :   
493            pass
494           
495    def display_version_and_quit(self) :
496        """Displays version number, then exists successfully."""
497        self.clean()
498        print version.__version__
499        sys.exit(0)
500   
501    def display_usage_and_quit(self) :
502        """Displays command line usage, then exists successfully."""
503        self.clean()
504        print self.documentation
505        sys.exit(0)
506       
507    def crashed(self, message) :   
508        """Outputs a crash message, and optionally sends it to software author."""
509        msg = crashed(message)
510        try :
511            crashrecipient = self.config.getCrashRecipient()
512            if crashrecipient :
513                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
514                fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
515                                (msg, \
516                                 "\n".join(["    %s" % repr(a) for a in sys.argv]), \
517                                 "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
518                server = smtplib.SMTP(self.smtpserver)
519                server.sendmail(admin, [admin, crashrecipient], \
520                                       "From: %s\nTo: %s\nCc: %s\nSubject: PyKota v%s crash traceback !\n\n%s" % \
521                                       (admin, crashrecipient, admin, version.__version__, fullmessage))
522                server.quit()
523        except :
524            pass
525       
526    def parseCommandline(self, argv, short, long, allownothing=0) :
527        """Parses the command line, controlling options."""
528        # split options in two lists: those which need an argument, those which don't need any
529        withoutarg = []
530        witharg = []
531        lgs = len(short)
532        i = 0
533        while i < lgs :
534            ii = i + 1
535            if (ii < lgs) and (short[ii] == ':') :
536                # needs an argument
537                witharg.append(short[i])
538                ii = ii + 1 # skip the ':'
539            else :
540                # doesn't need an argument
541                withoutarg.append(short[i])
542            i = ii
543               
544        for option in long :
545            if option[-1] == '=' :
546                # needs an argument
547                witharg.append(option[:-1])
548            else :
549                # doesn't need an argument
550                withoutarg.append(option)
551       
552        # we begin with all possible options unset
553        parsed = {}
554        for option in withoutarg + witharg :
555            parsed[option] = None
556       
557        # then we parse the command line
558        args = []       # to not break if something unexpected happened
559        try :
560            options, args = getopt.getopt(argv, short, long)
561            if options :
562                for (o, v) in options :
563                    # we skip the '-' chars
564                    lgo = len(o)
565                    i = 0
566                    while (i < lgo) and (o[i] == '-') :
567                        i = i + 1
568                    o = o[i:]
569                    if o in witharg :
570                        # needs an argument : set it
571                        parsed[o] = v
572                    elif o in withoutarg :
573                        # doesn't need an argument : boolean
574                        parsed[o] = 1
575                    else :
576                        # should never occur
577                        raise PyKotaToolError, "Unexpected problem when parsing command line"
578            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
579                self.display_usage_and_quit()
580        except getopt.error, msg :
581            self.printInfo(msg)
582            self.display_usage_and_quit()
583        return (parsed, args)
584   
585    def isValidName(self, name) :
586        """Checks if a user or printer name is valid."""
587        # unfortunately Python 2.1 string modules doesn't define ascii_letters...
588        asciiletters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
589        digits = '0123456789'
590        validchars = asciiletters + digits + "-_."
591        for c in name :
592            if c not in validchars :
593                return 0
594        return 1       
595       
596    def matchString(self, s, patterns) :
597        """Returns 1 if the string s matches one of the patterns, else 0."""
598        for pattern in patterns :
599            if fnmatch.fnmatchcase(s, pattern) :
600                return 1
601        return 0
602       
603    def sendMessage(self, adminmail, touser, fullmessage) :
604        """Sends an email message containing headers to some user."""
605        if "@" not in touser :
606            touser = "%s@%s" % (touser, self.maildomain or self.smtpserver)
607        try :   
608            server = smtplib.SMTP(self.smtpserver)
609        except socket.error, msg :   
610            self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
611        else :
612            try :
613                server.sendmail(adminmail, [touser], "From: %s\nTo: %s\n%s" % (adminmail, touser, fullmessage))
614            except smtplib.SMTPException, answer :   
615                for (k, v) in answer.recipients.items() :
616                    self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
617            server.quit()
618       
619    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
620        """Sends an email message to a user."""
621        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
622        self.sendMessage(adminmail, user.Email or user.Name, "Subject: %s\n\n%s" % (subject, message))
623       
624    def sendMessageToAdmin(self, adminmail, subject, message) :
625        """Sends an email message to the Print Quota administrator."""
626        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
627       
628    def _checkUserPQuota(self, userpquota) :           
629        """Checks the user quota on a printer and deny or accept the job."""
630        # then we check the user's own quota
631        # if we get there we are sure that policy is not EXTERNAL
632        user = userpquota.User
633        printer = userpquota.Printer
634        enforcement = self.config.getPrinterEnforcement(printer.Name)
635        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
636        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
637        if not userpquota.Exists :
638            # Unknown userquota
639            if policy == "ALLOW" :
640                action = "POLICY_ALLOW"
641            else :   
642                action = "POLICY_DENY"
643            self.printInfo(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
644        else :   
645            pagecounter = int(userpquota.PageCounter or 0)
646            if enforcement == "STRICT" :
647                pagecounter += self.softwareJobSize
648            if userpquota.SoftLimit is not None :
649                softlimit = int(userpquota.SoftLimit)
650                if pagecounter < softlimit :
651                    action = "ALLOW"
652                else :   
653                    if userpquota.HardLimit is None :
654                        # only a soft limit, this is equivalent to having only a hard limit
655                        action = "DENY"
656                    else :   
657                        hardlimit = int(userpquota.HardLimit)
658                        if softlimit <= pagecounter < hardlimit :   
659                            now = DateTime.now()
660                            if userpquota.DateLimit is not None :
661                                datelimit = DateTime.ISO.ParseDateTime(userpquota.DateLimit)
662                            else :
663                                datelimit = now + self.config.getGraceDelay(printer.Name)
664                                userpquota.setDateLimit(datelimit)
665                            if now < datelimit :
666                                action = "WARN"
667                            else :   
668                                action = "DENY"
669                        else :         
670                            action = "DENY"
671            else :       
672                if userpquota.HardLimit is not None :
673                    # no soft limit, only a hard one.
674                    hardlimit = int(userpquota.HardLimit)
675                    if pagecounter < hardlimit :
676                        action = "ALLOW"
677                    else :     
678                        action = "DENY"
679                else :
680                    # Both are unset, no quota, i.e. accounting only
681                    action = "ALLOW"
682        return action
683   
684    def checkGroupPQuota(self, grouppquota) :   
685        """Checks the group quota on a printer and deny or accept the job."""
686        group = grouppquota.Group
687        printer = grouppquota.Printer
688        enforcement = self.config.getPrinterEnforcement(printer.Name)
689        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
690        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
691            val = group.AccountBalance
692            if enforcement == "STRICT" : 
693                val -= self.softwareJobPrice # use precomputed size.
694            if val <= 0.0 :
695                action = "DENY"
696            elif val <= self.config.getPoorMan() :   
697                action = "WARN"
698            else :   
699                action = "ALLOW"
700            if (enforcement == "STRICT") and (val == 0.0) :
701                action = "WARN" # we can still print until account is 0
702        else :
703            val = grouppquota.PageCounter
704            if enforcement == "STRICT" :
705                val += self.softwareJobSize
706            if grouppquota.SoftLimit is not None :
707                softlimit = int(grouppquota.SoftLimit)
708                if val < softlimit :
709                    action = "ALLOW"
710                else :   
711                    if grouppquota.HardLimit is None :
712                        # only a soft limit, this is equivalent to having only a hard limit
713                        action = "DENY"
714                    else :   
715                        hardlimit = int(grouppquota.HardLimit)
716                        if softlimit <= val < hardlimit :   
717                            now = DateTime.now()
718                            if grouppquota.DateLimit is not None :
719                                datelimit = DateTime.ISO.ParseDateTime(grouppquota.DateLimit)
720                            else :
721                                datelimit = now + self.config.getGraceDelay(printer.Name)
722                                grouppquota.setDateLimit(datelimit)
723                            if now < datelimit :
724                                action = "WARN"
725                            else :   
726                                action = "DENY"
727                        else :         
728                            action = "DENY"
729            else :       
730                if grouppquota.HardLimit is not None :
731                    # no soft limit, only a hard one.
732                    hardlimit = int(grouppquota.HardLimit)
733                    if val < hardlimit :
734                        action = "ALLOW"
735                    else :     
736                        action = "DENY"
737                else :
738                    # Both are unset, no quota, i.e. accounting only
739                    action = "ALLOW"
740        return action
741   
742    def checkUserPQuota(self, userpquota) :
743        """Checks the user quota on a printer and all its parents and deny or accept the job."""
744        user = userpquota.User
745        printer = userpquota.Printer
746       
747        # indicates that a warning needs to be sent
748        warned = 0               
749       
750        # first we check any group the user is a member of
751        for group in self.storage.getUserGroups(user) :
752            grouppquota = self.storage.getGroupPQuota(group, printer)
753            # for the printer and all its parents
754            for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
755                if gpquota.Exists :
756                    action = self.checkGroupPQuota(gpquota)
757                    if action == "DENY" :
758                        return action
759                    elif action == "WARN" :   
760                        warned = 1
761                       
762        # Then we check the user's account balance
763        # if we get there we are sure that policy is not EXTERNAL
764        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
765        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
766            self.logdebug("Checking account balance for user %s" % user.Name)
767            if user.AccountBalance is None :
768                if policy == "ALLOW" :
769                    action = "POLICY_ALLOW"
770                else :   
771                    action = "POLICY_DENY"
772                self.printInfo(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
773                return action       
774            else :   
775                val = float(user.AccountBalance or 0.0)
776                enforcement = self.config.getPrinterEnforcement(printer.Name)
777                if enforcement == "STRICT" : 
778                    val -= self.softwareJobPrice # use precomputed size.
779                if val <= 0.0 :
780                    action = "DENY"
781                elif val <= self.config.getPoorMan() :   
782                    action = "WARN"
783                else :
784                    action = "ALLOW"
785                if (enforcement == "STRICT") and (val == 0.0) :
786                    action = "WARN" # we can still print until account is 0
787                return action   
788        else :
789            # Then check the user quota on current printer and all its parents.               
790            policyallowed = 0
791            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
792                action = self._checkUserPQuota(upquota)
793                if action in ("DENY", "POLICY_DENY") :
794                    return action
795                elif action == "WARN" :   
796                    warned = 1
797                elif action == "POLICY_ALLOW" :   
798                    policyallowed = 1
799            if warned :       
800                return "WARN"
801            elif policyallowed :   
802                return "POLICY_ALLOW" 
803            else :   
804                return "ALLOW"
805               
806    def externalMailTo(self, cmd, action, user, printer, message) :
807        """Warns the user with an external command."""
808        username = user.Name
809        printername = printer.Name
810        email = user.Email or user.Name
811        if "@" not in email :
812            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
813        os.system(cmd % locals())
814   
815    def formatCommandLine(self, cmd, user, printer) :
816        """Executes an external command."""
817        username = user.Name
818        printername = printer.Name
819        return cmd % locals()
820       
821    def warnGroupPQuota(self, grouppquota) :
822        """Checks a group quota and send messages if quota is exceeded on current printer."""
823        group = grouppquota.Group
824        printer = grouppquota.Printer
825        admin = self.config.getAdmin(printer.Name)
826        adminmail = self.config.getAdminMail(printer.Name)
827        (mailto, arguments) = self.config.getMailTo(printer.Name)
828        action = self.checkGroupPQuota(grouppquota)
829        if action.startswith("POLICY_") :
830            action = action[7:]
831        if action == "DENY" :
832            adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
833            self.printInfo(adminmessage)
834            if mailto in [ "BOTH", "ADMIN" ] :
835                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
836            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
837                for user in self.storage.getGroupMembers(group) :
838                    if mailto != "EXTERNAL" :
839                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
840                    else :   
841                        self.externalMailTo(arguments, action, user, printer, message)
842        elif action == "WARN" :   
843            adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
844            self.printInfo(adminmessage)
845            if mailto in [ "BOTH", "ADMIN" ] :
846                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
847            if group.LimitBy and (group.LimitBy.lower() == "balance") : 
848                message = self.config.getPoorWarn()
849            else :     
850                message = self.config.getSoftWarn(printer.Name)
851            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
852                for user in self.storage.getGroupMembers(group) :
853                    if mailto != "EXTERNAL" :
854                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
855                    else :   
856                        self.externalMailTo(arguments, action, user, printer, message)
857        return action       
858       
859    def warnUserPQuota(self, userpquota) :
860        """Checks a user quota and send him a message if quota is exceeded on current printer."""
861        user = userpquota.User
862        printer = userpquota.Printer
863        admin = self.config.getAdmin(printer.Name)
864        adminmail = self.config.getAdminMail(printer.Name)
865        (mailto, arguments) = self.config.getMailTo(printer.Name)
866        action = self.checkUserPQuota(userpquota)
867        if action.startswith("POLICY_") :
868            action = action[7:]
869        if action == "DENY" :
870            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
871            self.printInfo(adminmessage)
872            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
873                message = self.config.getHardWarn(printer.Name)
874                if mailto != "EXTERNAL" :
875                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
876                else :   
877                    self.externalMailTo(arguments, action, user, printer, message)
878            if mailto in [ "BOTH", "ADMIN" ] :
879                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
880        elif action == "WARN" :   
881            adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
882            self.printInfo(adminmessage)
883            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
884                if user.LimitBy and (user.LimitBy.lower() == "balance") : 
885                    message = self.config.getPoorWarn()
886                else :     
887                    message = self.config.getSoftWarn(printer.Name)
888                if mailto != "EXTERNAL" :   
889                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
890                else :   
891                    self.externalMailTo(arguments, action, user, printer, message)
892            if mailto in [ "BOTH", "ADMIN" ] :
893                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
894        return action       
895       
896class PyKotaFilterOrBackend(PyKotaTool) :   
897    """Class for the PyKota filter or backend."""
898    def __init__(self) :
899        """Initialize local datas from current environment."""
900        # We begin with ignoring signals, we may de-ignore them later on.
901        self.gotSigTerm = 0
902        signal.signal(signal.SIGTERM, signal.SIG_IGN)
903        # signal.signal(signal.SIGCHLD, signal.SIG_IGN)
904        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
905       
906        PyKotaTool.__init__(self)
907        (self.printingsystem, \
908         self.printerhostname, \
909         self.printername, \
910         self.username, \
911         self.jobid, \
912         self.inputfile, \
913         self.copies, \
914         self.title, \
915         self.options, \
916         self.originalbackend) = self.extractInfoFromCupsOrLprng()
917        self.username = self.username or 'root' # when printing test page from CUPS web interface, username is empty
918        if self.config.getUserNameToLower() :
919            self.username = self.username.lower()
920        self.preserveinputfile = self.inputfile 
921        try :
922            self.accounter = accounter.openAccounter(self)
923        except (config.PyKotaConfigError, accounter.PyKotaAccounterError), msg :   
924            self.crashed(msg)
925            raise
926        self.exportJobInfo()
927        self.jobdatastream = self.openJobDataStream()
928        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes)
929        self.logdebug("Job size is %s bytes" % self.jobSizeBytes)
930        self.logdebug("Capturing SIGTERM events.")
931        signal.signal(signal.SIGTERM, self.sigterm_handler)
932       
933    def openJobDataStream(self) :   
934        """Opens the file which contains the job's datas."""
935        if self.preserveinputfile is None :
936            # Job comes from sys.stdin, but this is not
937            # seekable and complexifies our task, so create
938            # a temporary file and use it instead
939            self.sendBackChannelData("Duplicating data stream from stdin to temporary file")
940            dummy = 0
941            MEGABYTE = 1024*1024
942            self.jobSizeBytes = 0
943            infile = tempfile.TemporaryFile()
944            while 1 :
945                data = sys.stdin.read(MEGABYTE) 
946                if not data :
947                    break
948                self.jobSizeBytes += len(data)   
949                if not (dummy % 10) :
950                    self.sendBackChannelData("%s bytes read..." % self.jobSizeBytes)
951                dummy += 1   
952                infile.write(data)
953            self.sendBackChannelData("%s bytes read total." % self.jobSizeBytes)
954            infile.flush()   
955            infile.seek(0)
956            return infile
957        else :   
958            # real file, just open it
959            self.sendBackChannelData("Opening data stream %s" % self.preserveinputfile)
960            self.jobSizeBytes = os.stat(self.preserveinputfile)[6]
961            return open(self.preserveinputfile, "rb")
962       
963    def closeJobDataStream(self) :   
964        """Closes the file which contains the job's datas."""
965        self.sendBackChannelData("Closing data stream.")
966        try :
967            self.jobdatastream.close()
968        except :   
969            pass
970       
971    def precomputeJobSize(self) :   
972        """Computes the job size with a software method."""
973        self.logdebug("Precomputing job's size with generic PDL analyzer...")
974        self.jobdatastream.seek(0)
975        try :
976            parser = pdlanalyzer.PDLAnalyzer(self.jobdatastream)
977            jobsize = parser.getJobSize()
978        except pdlanalyzer.PDLAnalyzerError, msg :   
979            # Here we just log the failure, but
980            # we finally ignore it and return 0 since this
981            # computation is just an indication of what the
982            # job's size MAY be.
983            self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
984            return 0
985        else :   
986            if ((self.printingsystem == "CUPS") \
987                and (self.preserveinputfile is not None)) \
988                or (self.printingsystem != "CUPS") :
989                return jobsize * self.copies
990            else :       
991                return jobsize
992           
993    def sigterm_handler(self, signum, frame) :
994        """Sets an attribute whenever SIGTERM is received."""
995        self.gotSigTerm = 1
996        os.environ["PYKOTASTATUS"] = "CANCELLED"
997        self.printInfo(_("SIGTERM received, job %s cancelled.") % self.jobid)
998       
999    def exportJobInfo(self) :   
1000        """Exports job information to the environment."""
1001        os.environ["PYKOTAUSERNAME"] = str(self.username)
1002        os.environ["PYKOTAPRINTERNAME"] = str(self.printername)
1003        os.environ["PYKOTAJOBID"] = str(self.jobid)
1004        os.environ["PYKOTATITLE"] = self.title or ""
1005        os.environ["PYKOTAFILENAME"] = self.preserveinputfile or ""
1006        os.environ["PYKOTACOPIES"] = str(self.copies)
1007        os.environ["PYKOTAOPTIONS"] = self.options or ""
1008   
1009    def exportUserInfo(self, userpquota) :
1010        """Exports user information to the environment."""
1011        os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy)
1012        os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0)
1013        os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0)
1014        os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0)
1015        os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0)
1016        os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit)
1017        os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit)
1018        os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit)
1019       
1020        # not really an user information, but anyway
1021        # exports the list of printers groups the current
1022        # printer is a member of
1023        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)])
1024       
1025    def prehook(self, userpquota) :
1026        """Allows plugging of an external hook before the job gets printed."""
1027        prehook = self.config.getPreHook(userpquota.Printer.Name)
1028        if prehook :
1029            self.logdebug("Executing pre-hook [%s]" % prehook)
1030            os.system(prehook)
1031       
1032    def posthook(self, userpquota) :
1033        """Allows plugging of an external hook after the job gets printed and/or denied."""
1034        posthook = self.config.getPostHook(userpquota.Printer.Name)
1035        if posthook :
1036            self.logdebug("Executing post-hook [%s]" % posthook)
1037            os.system(posthook)
1038       
1039    def printInfo(self, message, level="info") :       
1040        """Sends a message to standard error."""
1041        self.logger.log_message("%s" % message, level)
1042       
1043    def extractInfoFromCupsOrLprng(self) :   
1044        """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend).
1045       
1046           Returns (None, None, None, None, None, None, None, None, None, None) if no printing system is recognized.
1047        """
1048        # Try to detect CUPS
1049        if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) :
1050            if len(sys.argv) == 7 :
1051                inputfile = sys.argv[6]
1052            else :   
1053                inputfile = None
1054               
1055            # check that the DEVICE_URI environment variable's value is
1056            # prefixed with "cupspykota:" otherwise don't touch it.
1057            # If this is the case, we have to remove the prefix from
1058            # the environment before launching the real backend in cupspykota
1059            device_uri = os.environ.get("DEVICE_URI", "")
1060            if device_uri.startswith("cupspykota:") :
1061                fulldevice_uri = device_uri[:]
1062                device_uri = fulldevice_uri[len("cupspykota:"):]
1063                if device_uri.startswith("//") :    # lpd (at least)
1064                    device_uri = device_uri[2:]
1065                os.environ["DEVICE_URI"] = device_uri   # TODO : side effect !
1066            # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp
1067            try :
1068                (backend, destination) = device_uri.split(":", 1) 
1069            except ValueError :   
1070                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
1071            while destination.startswith("/") :
1072                destination = destination[1:]
1073            checkauth = destination.split("@", 1)   
1074            if len(checkauth) == 2 :
1075                destination = checkauth[1]
1076            printerhostname = destination.split("/")[0].split(":")[0]
1077            return ("CUPS", \
1078                    printerhostname, \
1079                    os.environ.get("PRINTER"), \
1080                    sys.argv[2].strip(), \
1081                    sys.argv[1].strip(), \
1082                    inputfile, \
1083                    int(sys.argv[4].strip()), \
1084                    sys.argv[3], \
1085                    sys.argv[5], \
1086                    backend)
1087        else :   
1088            # Try to detect LPRng
1089            # TODO : try to extract filename, job's title, and options if available
1090            jseen = Pseen = nseen = rseen = Kseen = None
1091            for arg in sys.argv :
1092                if arg.startswith("-j") :
1093                    jseen = arg[2:].strip()
1094                elif arg.startswith("-n") :     
1095                    nseen = arg[2:].strip()
1096                elif arg.startswith("-P") :   
1097                    Pseen = arg[2:].strip()
1098                elif arg.startswith("-r") :   
1099                    rseen = arg[2:].strip()
1100                elif arg.startswith("-K") or arg.startswith("-#") :   
1101                    Kseen = int(arg[2:].strip())
1102            if Kseen is None :       
1103                Kseen = 1       # we assume the user wants at least one copy...
1104            if (rseen is None) and jseen and Pseen and nseen :   
1105                self.printInfo(_("Printer hostname undefined, set to 'localhost'"), "warn")
1106                rseen = "localhost"
1107            if jseen and Pseen and nseen and rseen :       
1108                # job is always in stdin (None)
1109                return ("LPRNG", rseen, Pseen, nseen, jseen, None, Kseen, None, None, None)
1110        self.printInfo(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn")
1111        return (None, None, None, None, None, None, None, None, None, None)   # Unknown printing system
1112       
1113    def getPrinterUserAndUserPQuota(self) :       
1114        """Returns a tuple (policy, printer, user, and user print quota) on this printer.
1115       
1116           "OK" is returned in the policy if both printer, user and user print quota
1117           exist in the Quota Storage.
1118           Otherwise, the policy as defined for this printer in pykota.conf is returned.
1119           
1120           If policy was set to "EXTERNAL" and one of printer, user, or user print quota
1121           doesn't exist in the Quota Storage, then an external command is launched, as
1122           defined in the external policy for this printer in pykota.conf
1123           This external command can do anything, like automatically adding printers
1124           or users, for example, and finally extracting printer, user and user print
1125           quota from the Quota Storage is tried a second time.
1126           
1127           "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status
1128           was returned by the external command.
1129        """
1130        for passnumber in range(1, 3) :
1131            printer = self.storage.getPrinter(self.printername)
1132            user = self.storage.getUser(self.username)
1133            userpquota = self.storage.getUserPQuota(user, printer)
1134            if printer.Exists and user.Exists and userpquota.Exists :
1135                policy = "OK"
1136                break
1137            (policy, args) = self.config.getPrinterPolicy(self.printername)
1138            if policy == "EXTERNAL" :   
1139                commandline = self.formatCommandLine(args, user, printer)
1140                if not printer.Exists :
1141                    self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.printername, commandline, self.printername))
1142                if not user.Exists :
1143                    self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.username, commandline, self.printername))
1144                if not userpquota.Exists :
1145                    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))
1146                if os.system(commandline) :
1147                    self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.printername), "error")
1148                    policy = "EXTERNALERROR"
1149                    break
1150            else :       
1151                if not printer.Exists :
1152                    self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.printername, policy))
1153                if not user.Exists :
1154                    self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.username, policy, self.printername))
1155                if not userpquota.Exists :
1156                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.username, self.printername, policy))
1157                break
1158        if policy == "EXTERNAL" :   
1159            if not printer.Exists :
1160                self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.printername)
1161            if not user.Exists :
1162                self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.username, self.printername))
1163            if not userpquota.Exists :
1164                self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.username, self.printername))
1165        return (policy, printer, user, userpquota)
1166       
1167    def mainWork(self) :   
1168        """Main work is done here."""
1169        (policy, printer, user, userpquota) = self.getPrinterUserAndUserPQuota()
1170        # TODO : check for last user's quota in case pykota filter is used with querying
1171        if policy == "EXTERNALERROR" :
1172            # Policy was 'EXTERNAL' and the external command returned an error code
1173            return self.removeJob()
1174        elif policy == "EXTERNAL" :
1175            # Policy was 'EXTERNAL' and the external command wasn't able
1176            # to add either the printer, user or user print quota
1177            return self.removeJob()
1178        elif policy == "DENY" :   
1179            # Either printer, user or user print quota doesn't exist,
1180            # and the job should be rejected.
1181            return self.removeJob()
1182        else :
1183            if policy not in ("OK", "ALLOW") :
1184                self.printInfo(_("Invalid policy %s for printer %s") % (policy, self.printername))
1185                return self.removeJob()
1186            else :
1187                return self.doWork(policy, printer, user, userpquota)
Note: See TracBrowser for help on using the browser.