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

Revision 1923, 60.9 kB (checked in by jalet, 19 years ago)

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