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

Revision 1761, 54.9 kB (checked in by jalet, 20 years ago)

Should now correctly deal with charsets both when storing into databases and when
retrieving datas. Works with both PostgreSQL and LDAP.

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