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

Revision 2141, 64.2 kB (checked in by jerome, 19 years ago)

Testing the $Log$ keyword property

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