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

Revision 1918, 60.7 kB (checked in by jalet, 19 years ago)

PyKota banners now basically work !

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