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

Revision 1820, 56.1 kB (checked in by jalet, 20 years ago)

Made debugging levels be the same in cupspykota and lprngpykota.
Now outputs more information in informational messages : user, printer, jobid

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