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

Revision 1977, 62.8 kB (checked in by jalet, 19 years ago)

Fixed a bug when pkbanner's output was piped into another command (e.g. gs)

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