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

Revision 1766, 55.0 kB (checked in by jalet, 20 years ago)

Now exports the MD5 sum of the job's datas as an hexadecimal digest

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