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

Revision 1757, 54.1 kB (checked in by jalet, 20 years ago)

Added the winbind_separator directive to pykota.conf to allow the admin to
strip out the Samba/Winbind domain name when users print.

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