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

Revision 1911, 58.5 kB (checked in by jalet, 19 years ago)

Heavy work on pkbanner. Not finished yet though, but mostly works.

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