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

Revision 1600, 50.6 kB (checked in by jalet, 20 years ago)

LPRng support early version

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