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

Revision 1872, 57.9 kB (checked in by jalet, 20 years ago)

Added debug messages for LPRng support possible problem ???

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