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

Revision 1917, 60.5 kB (checked in by jalet, 19 years ago)

Continued integration of Matt's patch for banners

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