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

Revision 2060, 63.8 kB (checked in by jalet, 19 years ago)

Now always precomputes the job's size with the internal PDL parser, and not
only when 'enforcement: strict' was set in pykota.conf

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