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

Revision 1776, 55.3 kB (checked in by jalet, 20 years ago)

Small fix for errors caused by unknown locale

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