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

Revision 1848, 56.3 kB (checked in by jalet, 20 years ago)

Fixes recently introduced bug

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