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

Revision 2008, 64.7 kB (checked in by jalet, 19 years ago)

Regain priviledge the time to open the job's data file when printing in
raw mode with CUPS.

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