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

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

Removed unneeded import statement

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