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

Revision 1849, 56.4 kB (checked in by jalet, 20 years ago)

Now correctly logs command line arguments

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