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

Revision 1593, 50.1 kB (checked in by jalet, 20 years ago)

Reduced the set of invalid characters in names

  • 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.111  2004/07/06 18:09:42  jalet
25# Reduced the set of invalid characters in names
26#
27# Revision 1.110  2004/07/01 19:56:42  jalet
28# Better dispatching of error messages
29#
30# Revision 1.109  2004/07/01 17:45:49  jalet
31# Added code to handle the description field for printers
32#
33# Revision 1.108  2004/06/24 23:09:30  jalet
34# Also prints read size on last block
35#
36# Revision 1.107  2004/06/23 13:03:28  jalet
37# Catches accounter configuration errors earlier
38#
39# Revision 1.106  2004/06/22 09:31:18  jalet
40# Always send some debug info to CUPS' back channel stream (stderr) as
41# informationnal messages.
42#
43# Revision 1.105  2004/06/21 08:17:38  jalet
44# Added version number in subject message for directive crashrecipient.
45#
46# Revision 1.104  2004/06/18 13:34:49  jalet
47# Now all tracebacks include PyKota's version number
48#
49# Revision 1.103  2004/06/18 13:17:26  jalet
50# Now includes PyKota's version number in messages sent by the crashrecipient
51# directive.
52#
53# Revision 1.102  2004/06/17 13:26:51  jalet
54# Better exception handling code
55#
56# Revision 1.101  2004/06/16 20:56:34  jalet
57# Smarter initialisation code
58#
59# Revision 1.100  2004/06/11 08:16:03  jalet
60# More exceptions catched in case of very early failure.
61#
62# Revision 1.99  2004/06/11 07:07:38  jalet
63# Now detects and logs configuration syntax errors instead of failing without
64# any notice message.
65#
66# Revision 1.98  2004/06/08 19:27:12  jalet
67# Doesn't ignore SIGCHLD anymore
68#
69# Revision 1.97  2004/06/07 22:45:35  jalet
70# Now accepts a job when enforcement is STRICT and predicted account balance
71# is equal to 0.0 : since the job hasn't been printed yet, only its printing
72# will really render balance equal to 0.0, so we should be allowed to print.
73#
74# Revision 1.96  2004/06/05 22:18:04  jalet
75# Now catches some exceptions earlier.
76# storage.py and ldapstorage.py : removed old comments
77#
78# Revision 1.95  2004/06/03 21:50:34  jalet
79# Improved error logging.
80# crashrecipient directive added.
81# Now exports the job's size in bytes too.
82#
83# Revision 1.94  2004/06/03 08:51:03  jalet
84# logs job's size in bytes now
85#
86# Revision 1.93  2004/06/02 21:51:02  jalet
87# Moved the sigterm capturing elsewhere
88#
89# Revision 1.92  2004/06/02 13:21:38  jalet
90# Debug message added
91#
92# Revision 1.91  2004/05/25 05:17:52  jalet
93# Now precomputes the job's size only if current printer's enforcement
94# is "STRICT"
95#
96# Revision 1.90  2004/05/24 22:45:49  jalet
97# New 'enforcement' directive added
98# Polling loop improvements
99#
100# Revision 1.89  2004/05/21 22:02:52  jalet
101# Preliminary work on pre-accounting
102#
103# Revision 1.88  2004/05/18 14:49:20  jalet
104# Big code changes to completely remove the need for "requester" directives,
105# jsut use "hardware(... your previous requester directive's content ...)"
106#
107# Revision 1.87  2004/05/17 19:14:59  jalet
108# Now catches SIGPIPE and SIGCHLD
109#
110# Revision 1.86  2004/05/13 13:59:28  jalet
111# Code simplifications
112#
113# Revision 1.85  2004/05/11 08:26:27  jalet
114# Now catches connection problems to SMTP server
115#
116# Revision 1.84  2004/04/21 08:36:32  jalet
117# Exports the PYKOTASTATUS environment variable when SIGTERM is received.
118#
119# Revision 1.83  2004/04/16 17:03:49  jalet
120# The list of printers groups the current printer is a member of is
121# now exported in the PYKOTAPGROUPS environment variable
122#
123# Revision 1.82  2004/04/13 09:38:03  jalet
124# More work on correct child processes handling
125#
126# Revision 1.81  2004/04/09 22:24:47  jalet
127# Began work on correct handling of child processes when jobs are cancelled by
128# the user. Especially important when an external requester is running for a
129# long time.
130#
131# Revision 1.80  2004/04/06 12:00:21  jalet
132# uninitialized values caused problems
133#
134# Revision 1.79  2004/03/28 21:01:29  jalet
135# PYKOTALIMITBY environment variable is now exported too
136#
137# Revision 1.78  2004/03/08 20:13:25  jalet
138# Allow names to begin with a digit
139#
140# Revision 1.77  2004/03/03 13:10:35  jalet
141# Now catches all smtplib exceptions when there's a problem sending messages
142#
143# Revision 1.76  2004/03/01 14:34:15  jalet
144# PYKOTAPHASE wasn't set at the right time at the end of data transmission
145# to underlying layer (real backend)
146#
147# Revision 1.75  2004/03/01 11:23:25  jalet
148# Pre and Post hooks to external commands are available in the cupspykota
149# backend. Forthe pykota filter they will be implemented real soon now.
150#
151# Revision 1.74  2004/02/26 14:18:07  jalet
152# Should fix the remaining bugs wrt printers groups and users groups.
153#
154# Revision 1.73  2004/02/19 14:20:21  jalet
155# maildomain pykota.conf directive added.
156# Small improvements on mail headers quality.
157#
158# Revision 1.72  2004/01/14 15:51:19  jalet
159# Docstring added.
160#
161# Revision 1.71  2004/01/11 23:22:42  jalet
162# Major code refactoring, it's way cleaner, and now allows automated addition
163# of printers on first print.
164#
165# Revision 1.70  2004/01/08 14:10:32  jalet
166# Copyright year changed.
167#
168# Revision 1.69  2004/01/05 16:02:18  jalet
169# Dots in user, groups and printer names should be allowed.
170#
171# Revision 1.68  2004/01/02 17:38:40  jalet
172# This time it should be better...
173#
174# Revision 1.67  2004/01/02 17:37:09  jalet
175# I'm completely stupid !!! Better to not talk while coding !
176#
177# Revision 1.66  2004/01/02 17:31:26  jalet
178# Forgot to remove some code
179#
180# Revision 1.65  2003/12/06 08:14:38  jalet
181# Added support for CUPS device uris which contain authentication information.
182#
183# Revision 1.64  2003/11/29 22:03:17  jalet
184# Some code refactoring work. New code is not used at this time.
185#
186# Revision 1.63  2003/11/29 20:06:20  jalet
187# Added 'utolower' configuration option to convert all usernames to
188# lowercase when printing. All database accesses are still and will
189# remain case sensitive though.
190#
191# Revision 1.62  2003/11/25 22:37:22  jalet
192# Small code move
193#
194# Revision 1.61  2003/11/25 22:03:28  jalet
195# No more message on stderr when the translation is not available.
196#
197# Revision 1.60  2003/11/25 21:54:05  jalet
198# updated FAQ
199#
200# Revision 1.59  2003/11/25 13:33:43  jalet
201# Puts 'root' instead of '' when printing from CUPS web interface (which
202# gives an empty username)
203#
204# Revision 1.58  2003/11/21 14:28:45  jalet
205# More complete job history.
206#
207# Revision 1.57  2003/11/19 23:19:38  jalet
208# Code refactoring work.
209# Explicit redirection to /dev/null has to be set in external policy now, just
210# like in external mailto.
211#
212# Revision 1.56  2003/11/19 07:40:20  jalet
213# Missing import statement.
214# Better documentation for mailto: external(...)
215#
216# Revision 1.55  2003/11/18 23:43:12  jalet
217# Mailto can be any external command now, as usual.
218#
219# Revision 1.54  2003/10/24 21:52:46  jalet
220# Now can force language when coming from CGI script.
221#
222# Revision 1.53  2003/10/08 21:41:38  jalet
223# External policies for printers works !
224# We can now auto-add users on first print, and do other useful things if needed.
225#
226# Revision 1.52  2003/10/07 09:07:28  jalet
227# Character encoding added to please latest version of Python
228#
229# Revision 1.51  2003/10/06 14:21:41  jalet
230# Test reversed to not retrieve group members when no messages for them.
231#
232# Revision 1.50  2003/10/02 20:23:18  jalet
233# Storage caching mechanism added.
234#
235# Revision 1.49  2003/07/29 20:55:17  jalet
236# 1.14 is out !
237#
238# Revision 1.48  2003/07/21 23:01:56  jalet
239# Modified some messages aout soft limit
240#
241# Revision 1.47  2003/07/16 21:53:08  jalet
242# Really big modifications wrt new configuration file's location and content.
243#
244# Revision 1.46  2003/07/09 20:17:07  jalet
245# Email field added to PostgreSQL schema
246#
247# Revision 1.45  2003/07/08 19:43:51  jalet
248# Configurable warning messages.
249# Poor man's treshold value added.
250#
251# Revision 1.44  2003/07/07 11:49:24  jalet
252# Lots of small fixes with the help of PyChecker
253#
254# Revision 1.43  2003/07/04 09:06:32  jalet
255# Small bug fix wrt undefined "LimitBy" field.
256#
257# Revision 1.42  2003/06/30 12:46:15  jalet
258# Extracted reporting code.
259#
260# Revision 1.41  2003/06/25 14:10:01  jalet
261# Hey, it may work (edpykota --reset excepted) !
262#
263# Revision 1.40  2003/06/10 16:37:54  jalet
264# Deletion of the second user which is not needed anymore.
265# Added a debug configuration field in /etc/pykota.conf
266# All queries can now be sent to the logger in debug mode, this will
267# greatly help improve performance when time for this will come.
268#
269# Revision 1.39  2003/04/29 18:37:54  jalet
270# Pluggable accounting methods (actually doesn't support external scripts)
271#
272# Revision 1.38  2003/04/24 11:53:48  jalet
273# Default policy for unknown users/groups is to DENY printing instead
274# of the previous default to ALLOW printing. This is to solve an accuracy
275# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
276# (from PyKota's POV) will be charged to the next user who prints on the
277# same printer.
278#
279# Revision 1.37  2003/04/24 08:08:27  jalet
280# Debug message forgotten
281#
282# Revision 1.36  2003/04/24 07:59:40  jalet
283# LPRng support now works !
284#
285# Revision 1.35  2003/04/23 22:13:57  jalet
286# Preliminary support for LPRng added BUT STILL UNTESTED.
287#
288# Revision 1.34  2003/04/17 09:26:21  jalet
289# repykota now reports account balances too.
290#
291# Revision 1.33  2003/04/16 12:35:49  jalet
292# Groups quota work now !
293#
294# Revision 1.32  2003/04/16 08:53:14  jalet
295# Printing can now be limited either by user's account balance or by
296# page quota (the default). Quota report doesn't include account balance
297# yet, though.
298#
299# Revision 1.31  2003/04/15 11:30:57  jalet
300# More work done on money print charging.
301# Minor bugs corrected.
302# All tools now access to the storage as priviledged users, repykota excepted.
303#
304# Revision 1.30  2003/04/10 21:47:20  jalet
305# Job history added. Upgrade script neutralized for now !
306#
307# Revision 1.29  2003/03/29 13:45:27  jalet
308# GPL paragraphs were incorrectly (from memory) copied into the sources.
309# Two README files were added.
310# Upgrade script for PostgreSQL pre 1.01 schema was added.
311#
312# Revision 1.28  2003/03/29 13:08:28  jalet
313# Configuration is now expected to be found in /etc/pykota.conf instead of
314# in /etc/cups/pykota.conf
315# Installation script can move old config files to the new location if needed.
316# Better error handling if configuration file is absent.
317#
318# Revision 1.27  2003/03/15 23:01:28  jalet
319# New mailto option in configuration file added.
320# No time to test this tonight (although it should work).
321#
322# Revision 1.26  2003/03/09 23:58:16  jalet
323# Comment
324#
325# Revision 1.25  2003/03/07 22:56:14  jalet
326# 0.99 is out with some bug fixes.
327#
328# Revision 1.24  2003/02/27 23:48:41  jalet
329# Correctly maps PyKota's log levels to syslog log levels
330#
331# Revision 1.23  2003/02/27 22:55:20  jalet
332# WARN log priority doesn't exist.
333#
334# Revision 1.22  2003/02/27 09:09:20  jalet
335# Added a method to match strings against wildcard patterns
336#
337# Revision 1.21  2003/02/17 23:01:56  jalet
338# Typos
339#
340# Revision 1.20  2003/02/17 22:55:01  jalet
341# More options can now be set per printer or globally :
342#
343#       admin
344#       adminmail
345#       gracedelay
346#       requester
347#
348# the printer option has priority when both are defined.
349#
350# Revision 1.19  2003/02/10 11:28:45  jalet
351# Localization
352#
353# Revision 1.18  2003/02/10 01:02:17  jalet
354# External requester is about to work, but I must sleep
355#
356# Revision 1.17  2003/02/09 13:05:43  jalet
357# Internationalization continues...
358#
359# Revision 1.16  2003/02/09 12:56:53  jalet
360# Internationalization begins...
361#
362# Revision 1.15  2003/02/08 22:09:52  jalet
363# Name check method moved here
364#
365# Revision 1.14  2003/02/07 10:42:45  jalet
366# Indentation problem
367#
368# Revision 1.13  2003/02/07 08:34:16  jalet
369# Test wrt date limit was wrong
370#
371# Revision 1.12  2003/02/06 23:20:02  jalet
372# warnpykota doesn't need any user/group name argument, mimicing the
373# warnquota disk quota tool.
374#
375# Revision 1.11  2003/02/06 22:54:33  jalet
376# warnpykota should be ok
377#
378# Revision 1.10  2003/02/06 15:03:11  jalet
379# added a method to set the limit date
380#
381# Revision 1.9  2003/02/06 10:39:23  jalet
382# Preliminary edpykota work.
383#
384# Revision 1.8  2003/02/06 09:19:02  jalet
385# More robust behavior (hopefully) when the user or printer is not managed
386# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
387# printer and/or user not 'yet?' in storage.
388#
389# Revision 1.7  2003/02/06 00:00:45  jalet
390# Now includes the printer name in email messages
391#
392# Revision 1.6  2003/02/05 23:55:02  jalet
393# Cleaner email messages
394#
395# Revision 1.5  2003/02/05 23:45:09  jalet
396# Better DateTime manipulation wrt grace delay
397#
398# Revision 1.4  2003/02/05 23:26:22  jalet
399# Incorrect handling of grace delay
400#
401# Revision 1.3  2003/02/05 22:16:20  jalet
402# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
403#
404# Revision 1.2  2003/02/05 22:10:29  jalet
405# Typos
406#
407# Revision 1.1  2003/02/05 21:28:17  jalet
408# Initial import into CVS
409#
410#
411#
412
413import sys
414import os
415import fnmatch
416import getopt
417import smtplib
418import gettext
419import locale
420import signal
421import socket
422import tempfile
423import ConfigParser
424
425from mx import DateTime
426
427from pykota import version, config, storage, logger, accounter, pdlanalyzer
428
429class PyKotaToolError(Exception):
430    """An exception for PyKota config related stuff."""
431    def __init__(self, message = ""):
432        self.message = message
433        Exception.__init__(self, message)
434    def __repr__(self):
435        return self.message
436    __str__ = __repr__
437   
438def crashed(message) :   
439    """Minimal crash method."""
440    import traceback
441    lines = []
442    for line in traceback.format_exception(*sys.exc_info()) :
443        lines.extend([l for l in line.split("\n") if l])
444    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % version.__version__, message] + lines)])
445    sys.stderr.write(msg)
446    sys.stderr.flush()
447    return msg
448
449class PyKotaTool :   
450    """Base class for all PyKota command line tools."""
451    def __init__(self, lang=None, doc="PyKota %s (c) 2003-2004 %s" % (version.__version__, version.__author__)) :
452        """Initializes the command line tool."""
453        # locale stuff
454        try :
455            locale.setlocale(locale.LC_ALL, lang)
456            gettext.install("pykota")
457        except (locale.Error, IOError) :
458            gettext.NullTranslations().install()
459   
460        # pykota specific stuff
461        self.documentation = doc
462        try :
463            self.config = config.PyKotaConfig("/etc/pykota")
464        except ConfigParser.ParsingError, msg :   
465            sys.stderr.write("ERROR: Problem encountered while parsing configuration file : %s\n" % msg)
466            sys.stderr.flush()
467            sys.exit(-1)
468           
469        try :
470            self.debug = self.config.getDebug()
471            self.smtpserver = self.config.getSMTPServer()
472            self.maildomain = self.config.getMailDomain()
473            self.logger = logger.openLogger(self.config.getLoggingBackend())
474            self.storage = storage.openConnection(self)
475        except (config.PyKotaConfigError, logger.PyKotaLoggingError, storage.PyKotaStorageError), msg :
476            self.crashed(msg)
477            raise
478        self.softwareJobSize = 0
479        self.softwareJobPrice = 0.0
480       
481    def logdebug(self, message) :   
482        """Logs something to debug output if debug is enabled."""
483        if self.debug :
484            self.logger.log_message(message, "debug")
485           
486    def printInfo(self, message, level="info") :       
487        """Sends a message to standard error."""
488        sys.stderr.write("%s: %s\n" % (level.upper(), message))
489        sys.stderr.flush()
490       
491    def clean(self) :   
492        """Ensures that the database is closed."""
493        try :
494            self.storage.close()
495        except (TypeError, NameError, AttributeError) :   
496            pass
497           
498    def display_version_and_quit(self) :
499        """Displays version number, then exists successfully."""
500        self.clean()
501        print version.__version__
502        sys.exit(0)
503   
504    def display_usage_and_quit(self) :
505        """Displays command line usage, then exists successfully."""
506        self.clean()
507        print self.documentation
508        sys.exit(0)
509       
510    def crashed(self, message) :   
511        """Outputs a crash message, and optionally sends it to software author."""
512        msg = crashed(message)
513        try :
514            crashrecipient = self.config.getCrashRecipient()
515            if crashrecipient :
516                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
517                fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
518                                (msg, \
519                                 "\n".join(["    %s" % repr(a) for a in sys.argv]), \
520                                 "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
521                server = smtplib.SMTP(self.smtpserver)
522                server.sendmail(admin, [admin, crashrecipient], \
523                                       "From: %s\nTo: %s\nCc: %s\nSubject: PyKota v%s crash traceback !\n\n%s" % \
524                                       (admin, crashrecipient, admin, version.__version__, fullmessage))
525                server.quit()
526        except :
527            pass
528       
529    def parseCommandline(self, argv, short, long, allownothing=0) :
530        """Parses the command line, controlling options."""
531        # split options in two lists: those which need an argument, those which don't need any
532        withoutarg = []
533        witharg = []
534        lgs = len(short)
535        i = 0
536        while i < lgs :
537            ii = i + 1
538            if (ii < lgs) and (short[ii] == ':') :
539                # needs an argument
540                witharg.append(short[i])
541                ii = ii + 1 # skip the ':'
542            else :
543                # doesn't need an argument
544                withoutarg.append(short[i])
545            i = ii
546               
547        for option in long :
548            if option[-1] == '=' :
549                # needs an argument
550                witharg.append(option[:-1])
551            else :
552                # doesn't need an argument
553                withoutarg.append(option)
554       
555        # we begin with all possible options unset
556        parsed = {}
557        for option in withoutarg + witharg :
558            parsed[option] = None
559       
560        # then we parse the command line
561        args = []       # to not break if something unexpected happened
562        try :
563            options, args = getopt.getopt(argv, short, long)
564            if options :
565                for (o, v) in options :
566                    # we skip the '-' chars
567                    lgo = len(o)
568                    i = 0
569                    while (i < lgo) and (o[i] == '-') :
570                        i = i + 1
571                    o = o[i:]
572                    if o in witharg :
573                        # needs an argument : set it
574                        parsed[o] = v
575                    elif o in withoutarg :
576                        # doesn't need an argument : boolean
577                        parsed[o] = 1
578                    else :
579                        # should never occur
580                        raise PyKotaToolError, "Unexpected problem when parsing command line"
581            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
582                self.display_usage_and_quit()
583        except getopt.error, msg :
584            self.printInfo(msg)
585            self.display_usage_and_quit()
586        return (parsed, args)
587   
588    def isValidName(self, name) :
589        """Checks if a user or printer name is valid."""
590        invalidchars = "/@"
591        for c in invalidchars :
592            if c in name :
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.