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

Revision 2093, 64.2 kB (checked in by jalet, 19 years ago)

Now logs the type of user running the command : mere mortal vs PyKota Admin

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