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

Revision 1803, 55.7 kB (checked in by jalet, 20 years ago)

Postponed string interpolation to help message's output method

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