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

Revision 1725, 52.8 kB (checked in by jalet, 20 years ago)

Re-Extends the list of invalid characters in names to prevent
people from adding user "*" for example, or to prevent
print administrators to hijack the system by putting dangerous
datas into the database which would cause commands later run by root
to compromise the system.

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