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

Revision 1944, 61.9 kB (checked in by jalet, 19 years ago)

Fix for the fix

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