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

Revision 1846, 56.2 kB (checked in by jalet, 20 years ago)

Fixed incorrect setting of the user's locale

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