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

Revision 1795, 55.5 kB (checked in by jalet, 20 years ago)

Adds fake translation marker

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