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

Revision 2006, 64.5 kB (checked in by jalet, 19 years ago)

Implemented the dropping of priviledges. Beware, beware...

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