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

Revision 1960, 62.6 kB (checked in by jalet, 19 years ago)

Now PyKota searches its configuration files first in system user pykota's
home directory

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