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

Revision 1851, 57.1 kB (checked in by jalet, 20 years ago)

Another fix for charset detection and Python2.3

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