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

Revision 1932, 61.6 kB (checked in by jalet, 19 years ago)

Fix for LPRng job's file detection code

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