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

Revision 1943, 61.8 kB (checked in by jalet, 19 years ago)

Fix problem with spaces at the end of filenames in LPRng

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