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

Revision 2054, 65.5 kB (checked in by jalet, 19 years ago)

Big database structure changes. Upgrade script is now included as well as
the new LDAP schema.
Introduction of the -o | --overcharge command line option to edpykota.
The output of repykota is more complete, but doesn't fit in 80 columns anymore.
Introduction of the new 'maxdenybanners' directive.

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