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

Revision 1856, 57.6 kB (checked in by jalet, 20 years ago)

Now logs something when locale settings are incorrect.

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