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

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

Another test

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