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

Revision 1582, 50.3 kB (checked in by jalet, 20 years ago)

Added code to handle the description field for printers

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