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

Revision 1898, 58.0 kB (checked in by jalet, 19 years ago)

Little change for locale+gettext

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