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

Revision 2143, 64.3 kB (checked in by jerome, 19 years ago)

Test some additional keywords

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