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

Revision 3184, 35.5 kB (checked in by jerome, 17 years ago)

Added some docstrings.

  • 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, 2005, 2006, 2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
25"""This module defines the base classes for PyKota command line tools."""
26
27import sys
28import os
29import pwd
30import fnmatch
31import getopt
32import smtplib
33import gettext
34import locale
35import socket
36import time
37from email.MIMEText import MIMEText
38from email.Header import Header
39import email.Utils
40
41from mx import DateTime
42
43try :
44    import chardet
45except ImportError :   
46    def detectCharset(text) :
47        """Fakes a charset detection if the chardet module is not installed."""
48        return "ISO-8859-15"
49else :   
50    def detectCharset(text) :
51        """Uses the chardet module to workaround CUPS lying to us."""
52        return chardet.detect(text)["encoding"]
53
54from pykota import config, storage, logger
55from pykota.version import __version__, __author__, __years__, __gplblurb__
56
57def N_(message) :
58    """Fake translation marker for translatable strings extraction."""
59    return message
60
61class PyKotaToolError(Exception):
62    """An exception for PyKota related stuff."""
63    def __init__(self, message = ""):
64        self.message = message
65        Exception.__init__(self, message)
66    def __repr__(self):
67        return self.message
68    __str__ = __repr__
69   
70class PyKotaCommandLineError(PyKotaToolError) :   
71    """An exception for Pykota command line tools."""
72    pass
73   
74def crashed(message="Bug in PyKota") :   
75    """Minimal crash method."""
76    import traceback
77    lines = []
78    for line in traceback.format_exception(*sys.exc_info()) :
79        lines.extend([l for l in line.split("\n") if l])
80    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % __version__, message] + lines)])
81    sys.stderr.write(msg)
82    sys.stderr.flush()
83    return msg
84
85class Percent :
86    """A class to display progress."""
87    def __init__(self, app, size=None) :
88        """Initializes the engine."""
89        self.app = app
90        self.size = None
91        if size :
92            self.setSize(size)
93        self.previous = None
94        self.before = time.time()
95       
96    def setSize(self, size) :     
97        """Sets the total size."""
98        self.number = 0
99        self.size = size
100        if size :
101            self.factor = 100.0 / float(size)
102       
103    def display(self, msg) :   
104        """Displays the value."""
105        self.app.display(msg)
106       
107    def oneMore(self) :   
108        """Increments internal counter."""
109        if self.size :
110            self.number += 1
111            percent = "%.02f" % (float(self.number) * self.factor)
112            if percent != self.previous : # optimize for large number of items
113                self.display("\r%s%%" % percent)
114                self.previous = percent
115           
116    def done(self) :         
117        """Displays the 'done' message."""
118        after = time.time()
119        if self.size :
120            speed = self.size / (after - self.before)
121            self.display("\r100.00%%\r        \r%s. %s : %.2f %s.\n" \
122                     % (_("Done"), _("Average speed"), speed, _("entries per second")))
123        else :             
124            self.display("\r100.00%%\r        \r%s.\n" % _("Done"))
125       
126class Tool :
127    """Base class for tools with no database access."""
128    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
129        """Initializes the command line tool."""
130        # did we drop priviledges ?
131        self.privdropped = 0
132       
133        # locale stuff
134        try :
135            locale.setlocale(locale.LC_ALL, (lang, charset))
136        except (locale.Error, IOError) :
137            locale.setlocale(locale.LC_ALL, None)
138        (self.language, self.charset) = locale.getlocale()
139        self.language = self.language or "C"
140        try :
141            self.charset = self.charset or locale.getpreferredencoding()
142        except locale.Error :   
143            self.charset = sys.getfilesystemencoding()
144       
145        # Dirty hack : if the charset is ASCII, we can safely use UTF-8 instead
146        # This has the advantage of allowing transparent support for recent
147        # versions of CUPS which (en-)force charset to UTF-8 when printing.
148        # This should be needed only when printing, but is probably (?) safe
149        # to do when using interactive commands.
150        if self.charset.upper() in ('ASCII', 'ANSI_X3.4-1968') :
151            self.charset = "UTF-8"
152       
153        # translation stuff
154        try :
155            try :
156                trans = gettext.translation("pykota", languages=["%s.%s" % (self.language, self.charset)], codeset=self.charset)
157            except TypeError : # Python <2.4
158                trans = gettext.translation("pykota", languages=["%s.%s" % (self.language, self.charset)])
159            trans.install()
160        except :
161            gettext.NullTranslations().install()
162   
163        # pykota specific stuff
164        self.documentation = doc
165       
166    def deferredInit(self) :       
167        """Deferred initialization."""
168        confdir = os.environ.get("PYKOTA_HOME")
169        environHome = True
170        missingUser = False
171        if confdir is None :
172            environHome = False
173            # check for config files in the 'pykota' user's home directory.
174            try :
175                self.pykotauser = pwd.getpwnam("pykota")
176                confdir = self.pykotauser[5]
177            except KeyError :   
178                self.pykotauser = None
179                confdir = "/etc/pykota"
180                missingUser = True
181           
182        self.config = config.PyKotaConfig(confdir)
183        self.debug = self.config.getDebug()
184        self.smtpserver = self.config.getSMTPServer()
185        self.maildomain = self.config.getMailDomain()
186        self.logger = logger.openLogger(self.config.getLoggingBackend())
187           
188        # now drop priviledge if possible
189        self.dropPriv()   
190       
191        # We NEED this here, even when not in an accounting filter/backend   
192        self.softwareJobSize = 0
193        self.softwareJobPrice = 0.0
194       
195        if environHome :
196            self.printInfo("PYKOTA_HOME environment variable is set. Configuration files were searched in %s" % confdir, "info")
197        else :
198            if missingUser :     
199                self.printInfo("The 'pykota' system account is missing. Configuration files were searched in %s instead." % confdir, "warn")
200       
201        self.logdebug("Language in use : %s" % self.language)
202        self.logdebug("Charset in use : %s" % self.charset)
203       
204        arguments = " ".join(['"%s"' % arg for arg in sys.argv])
205        self.logdebug("Command line arguments : %s" % arguments)
206       
207    def dropPriv(self) :   
208        """Drops priviledges."""
209        uid = os.geteuid()
210        try :
211            self.originalUserName = pwd.getpwuid(uid)[0]
212        except (KeyError, IndexError), msg :   
213            self.printInfo(_("Strange problem with uid(%s) : %s") % (uid, msg), "warn")
214            self.originalUserName = None
215        else :
216            if uid :
217                self.logdebug(_("Running as user '%s'.") % self.originalUserName)
218            else :
219                if self.pykotauser is None :
220                    self.logdebug(_("No user named 'pykota'. Not dropping priviledges."))
221                else :   
222                    try :
223                        os.setegid(self.pykotauser[3])
224                        os.seteuid(self.pykotauser[2])
225                    except OSError, msg :   
226                        self.printInfo(_("Impossible to drop priviledges : %s") % msg, "warn")
227                    else :   
228                        self.logdebug(_("Priviledges dropped. Now running as user 'pykota'."))
229                        self.privdropped = 1
230           
231    def regainPriv(self) :   
232        """Drops priviledges."""
233        if self.privdropped :
234            try :
235                os.seteuid(0)
236                os.setegid(0)
237            except OSError, msg :   
238                self.printInfo(_("Impossible to regain priviledges : %s") % msg, "warn")
239            else :   
240                self.logdebug(_("Regained priviledges. Now running as root."))
241                self.privdropped = 0
242       
243    def UTF8ToUserCharset(self, text) :
244        """Converts from UTF-8 to user's charset."""
245        if text is None :
246            return None
247        try :
248            return text.decode("UTF-8").encode(self.charset, "replace") 
249        except (UnicodeError, AttributeError) :   
250            try :
251                # Maybe already in Unicode ?
252                return text.encode(self.charset, "replace") 
253            except (UnicodeError, AttributeError) :
254                # Try to autodetect the charset
255                return text.decode(detectCharset(text), "replace").encode(self.charset, "replace")
256       
257    def userCharsetToUTF8(self, text) :
258        """Converts from user's charset to UTF-8."""
259        if text is None :
260            return None
261        try :
262            # We don't necessarily trust the default charset, because
263            # xprint sends us titles in UTF-8 but CUPS gives us an ISO-8859-1 charset !
264            # So we first try to see if the text is already in UTF-8 or not, and
265            # if it is, we delete characters which can't be converted to the user's charset,
266            # then convert back to UTF-8. PostgreSQL 7.3.x used to reject some unicode characters,
267            # this is fixed by the ugly line below :
268            return text.decode("UTF-8").encode(self.charset, "replace").decode(self.charset).encode("UTF-8", "replace")
269        except (UnicodeError, AttributeError) :
270            try :
271                return text.decode(self.charset).encode("UTF-8", "replace") 
272            except (UnicodeError, AttributeError) :   
273                try :
274                    # Maybe already in Unicode ?
275                    return text.encode("UTF-8", "replace") 
276                except (UnicodeError, AttributeError) :
277                    # Try to autodetect the charset
278                    return text.decode(detectCharset(text), "replace").encode("UTF-8", "replace")
279        return newtext
280       
281    def display(self, message) :
282        """Display a message but only if stdout is a tty."""
283        if sys.stdout.isatty() :
284            sys.stdout.write(message)
285            sys.stdout.flush()
286           
287    def logdebug(self, message) :   
288        """Logs something to debug output if debug is enabled."""
289        if self.debug :
290            self.logger.log_message(message, "debug")
291           
292    def printInfo(self, message, level="info") :       
293        """Sends a message to standard error."""
294        sys.stderr.write("%s: %s\n" % (level.upper(), message))
295        sys.stderr.flush()
296       
297    def matchString(self, s, patterns) :
298        """Returns True if the string s matches one of the patterns, else False."""
299        if not patterns :
300            return True # No pattern, always matches.
301        else :   
302            for pattern in patterns :
303                if fnmatch.fnmatchcase(s, pattern) :
304                    return True
305            return False
306       
307    def sanitizeNames(self, options, names) :
308        """Ensures that an user can only see the datas he is allowed to see, by modifying the list of names."""
309        if not self.config.isAdmin :
310            username = pwd.getpwuid(os.geteuid())[0]
311            if not options["list"] :
312                raise PyKotaCommandLineError, "%s : %s" % (username, _("You're not allowed to use this command."))
313            else :
314                if options["groups"] :
315                    user = self.storage.getUser(username)
316                    if user.Exists :
317                        return [ g.Name for g in self.storage.getUserGroups(user) ]
318                return [ username ]
319        elif not names :       
320            return ["*"]
321        else :   
322            return names
323       
324    def display_version_and_quit(self) :
325        """Displays version number, then exists successfully."""
326        try :
327            self.clean()
328        except AttributeError :   
329            pass
330        print __version__
331        sys.exit(0)
332   
333    def display_usage_and_quit(self) :
334        """Displays command line usage, then exists successfully."""
335        try :
336            self.clean()
337        except AttributeError :   
338            pass
339        print _(self.documentation) % globals()
340        print __gplblurb__
341        print
342        print _("Please report bugs to :"), __author__
343        sys.exit(0)
344       
345    def crashed(self, message="Bug in PyKota") :   
346        """Outputs a crash message, and optionally sends it to software author."""
347        msg = crashed(message)
348        fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
349                        (msg, \
350                         "\n".join(["    %s" % repr(a) for a in sys.argv]), \
351                         "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
352        try :
353            crashrecipient = self.config.getCrashRecipient()
354            if crashrecipient :
355                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
356                server = smtplib.SMTP(self.smtpserver)
357                msg = MIMEText(fullmessage, _charset=self.charset)
358                msg["Subject"] = Header("PyKota v%s crash traceback !" \
359                                        % __version__, charset=self.charset)
360                msg["From"] = admin
361                msg["To"] = crashrecipient
362                msg["Cc"] = admin
363                msg["Date"] = email.Utils.formatdate(localtime=True)
364                server.sendmail(admin, [admin, crashrecipient], msg.as_string())
365                server.quit()
366        except :
367            pass
368        return fullmessage   
369       
370    def parseCommandline(self, argv, short, long, allownothing=0) :
371        """Parses the command line, controlling options."""
372        # split options in two lists: those which need an argument, those which don't need any
373        short = "%sA:" % short
374        long.append("arguments=")
375        withoutarg = []
376        witharg = []
377        lgs = len(short)
378        i = 0
379        while i < lgs :
380            ii = i + 1
381            if (ii < lgs) and (short[ii] == ':') :
382                # needs an argument
383                witharg.append(short[i])
384                ii = ii + 1 # skip the ':'
385            else :
386                # doesn't need an argument
387                withoutarg.append(short[i])
388            i = ii
389               
390        for option in long :
391            if option[-1] == '=' :
392                # needs an argument
393                witharg.append(option[:-1])
394            else :
395                # doesn't need an argument
396                withoutarg.append(option)
397       
398        # then we parse the command line
399        done = 0
400        while not done :
401            # we begin with all possible options unset
402            parsed = {}
403            for option in withoutarg + witharg :
404                parsed[option] = None
405            args = []       # to not break if something unexpected happened
406            try :
407                options, args = getopt.getopt(argv, short, long)
408                if options :
409                    for (o, v) in options :
410                        # we skip the '-' chars
411                        lgo = len(o)
412                        i = 0
413                        while (i < lgo) and (o[i] == '-') :
414                            i = i + 1
415                        o = o[i:]
416                        if o in witharg :
417                            # needs an argument : set it
418                            parsed[o] = v
419                        elif o in withoutarg :
420                            # doesn't need an argument : boolean
421                            parsed[o] = 1
422                        else :
423                            # should never occur
424                            raise PyKotaCommandLineError, "Unexpected problem when parsing command line"
425                elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
426                    self.display_usage_and_quit()
427            except getopt.error, msg :
428                raise PyKotaCommandLineError, str(msg)
429            else :   
430                if parsed["arguments"] or parsed["A"] :
431                    # arguments are in a file, we ignore all other arguments
432                    # and reset the list of arguments to the lines read from
433                    # the file.
434                    argsfile = open(parsed["arguments"] or parsed["A"], "r")
435                    argv = [ l.strip() for l in argsfile.readlines() ]
436                    argsfile.close()
437                    for i in range(len(argv)) :
438                        argi = argv[i]
439                        if argi.startswith('"') and argi.endswith('"') :
440                            argv[i] = argi[1:-1]
441                else :   
442                    done = 1
443        return (parsed, args)
444   
445class PyKotaTool(Tool) :   
446    """Base class for all PyKota command line tools."""
447    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
448        """Initializes the command line tool and opens the database."""
449        Tool.__init__(self, lang, charset, doc)
450       
451    def deferredInit(self) :   
452        """Deferred initialization."""
453        Tool.deferredInit(self)
454        self.storage = storage.openConnection(self)
455        if self.config.isAdmin : # TODO : We don't know this before, fix this !
456            self.logdebug("Beware : running as a PyKota administrator !")
457        else :   
458            self.logdebug("Don't Panic : running as a mere mortal !")
459       
460    def clean(self) :   
461        """Ensures that the database is closed."""
462        try :
463            self.storage.close()
464        except (TypeError, NameError, AttributeError) :   
465            pass
466           
467    def isValidName(self, name) :
468        """Checks if a user or printer name is valid."""
469        invalidchars = "/@?*,;&|"
470        for c in list(invalidchars) :
471            if c in name :
472                return 0
473        return 1       
474       
475    def sendMessage(self, adminmail, touser, fullmessage) :
476        """Sends an email message containing headers to some user."""
477        try :   
478            server = smtplib.SMTP(self.smtpserver)
479        except socket.error, msg :   
480            self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
481        else :
482            try :
483                server.sendmail(adminmail, [touser], fullmessage)
484            except smtplib.SMTPException, answer :   
485                for (k, v) in answer.recipients.items() :
486                    self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
487            server.quit()
488       
489    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
490        """Sends an email message to a user."""
491        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
492        usermail = user.Email or user.Name
493        if "@" not in usermail :
494            usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver or "localhost")
495        msg = MIMEText(message, _charset=self.charset)
496        msg["Subject"] = Header(subject, charset=self.charset)
497        msg["From"] = adminmail
498        msg["To"] = usermail
499        msg["Date"] = email.Utils.formatdate(localtime=True)
500        self.sendMessage(adminmail, usermail, msg.as_string())
501       
502    def sendMessageToAdmin(self, adminmail, subject, message) :
503        """Sends an email message to the Print Quota administrator."""
504        if "@" not in adminmail :
505            adminmail = "%s@%s" % (adminmail, self.maildomain or self.smtpserver or "localhost")
506        msg = MIMEText(message, _charset=self.charset)
507        msg["Subject"] = Header(subject, charset=self.charset)
508        msg["From"] = adminmail
509        msg["To"] = adminmail
510        self.sendMessage(adminmail, adminmail, msg.as_string())
511       
512    def _checkUserPQuota(self, userpquota) :           
513        """Checks the user quota on a printer and deny or accept the job."""
514        # then we check the user's own quota
515        # if we get there we are sure that policy is not EXTERNAL
516        user = userpquota.User
517        printer = userpquota.Printer
518        enforcement = self.config.getPrinterEnforcement(printer.Name)
519        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
520        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
521        if not userpquota.Exists :
522            # Unknown userquota
523            if policy == "ALLOW" :
524                action = "POLICY_ALLOW"
525            else :   
526                action = "POLICY_DENY"
527            self.printInfo(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
528        else :   
529            pagecounter = int(userpquota.PageCounter or 0)
530            if enforcement == "STRICT" :
531                pagecounter += self.softwareJobSize
532            if userpquota.SoftLimit is not None :
533                softlimit = int(userpquota.SoftLimit)
534                if pagecounter < softlimit :
535                    action = "ALLOW"
536                else :   
537                    if userpquota.HardLimit is None :
538                        # only a soft limit, this is equivalent to having only a hard limit
539                        action = "DENY"
540                    else :   
541                        hardlimit = int(userpquota.HardLimit)
542                        if softlimit <= pagecounter < hardlimit :   
543                            now = DateTime.now()
544                            if userpquota.DateLimit is not None :
545                                datelimit = DateTime.ISO.ParseDateTime(str(userpquota.DateLimit)[:19])
546                            else :
547                                datelimit = now + self.config.getGraceDelay(printer.Name)
548                                userpquota.setDateLimit(datelimit)
549                            if now < datelimit :
550                                action = "WARN"
551                            else :   
552                                action = "DENY"
553                        else :         
554                            action = "DENY"
555            else :       
556                if userpquota.HardLimit is not None :
557                    # no soft limit, only a hard one.
558                    hardlimit = int(userpquota.HardLimit)
559                    if pagecounter < hardlimit :
560                        action = "ALLOW"
561                    else :     
562                        action = "DENY"
563                else :
564                    # Both are unset, no quota, i.e. accounting only
565                    action = "ALLOW"
566        return action
567   
568    def checkGroupPQuota(self, grouppquota) :   
569        """Checks the group quota on a printer and deny or accept the job."""
570        group = grouppquota.Group
571        printer = grouppquota.Printer
572        enforcement = self.config.getPrinterEnforcement(printer.Name)
573        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
574        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
575            val = group.AccountBalance or 0.0
576            if enforcement == "STRICT" : 
577                val -= self.softwareJobPrice # use precomputed size.
578            balancezero = self.config.getBalanceZero()
579            if val <= balancezero :
580                action = "DENY"
581            elif val <= self.config.getPoorMan() :   
582                action = "WARN"
583            else :   
584                action = "ALLOW"
585            if (enforcement == "STRICT") and (val == balancezero) :
586                action = "WARN" # we can still print until account is 0
587        else :
588            val = grouppquota.PageCounter or 0
589            if enforcement == "STRICT" :
590                val += int(self.softwareJobSize) # TODO : this is not a fix, problem is elsewhere in grouppquota.PageCounter
591            if grouppquota.SoftLimit is not None :
592                softlimit = int(grouppquota.SoftLimit)
593                if val < softlimit :
594                    action = "ALLOW"
595                else :   
596                    if grouppquota.HardLimit is None :
597                        # only a soft limit, this is equivalent to having only a hard limit
598                        action = "DENY"
599                    else :   
600                        hardlimit = int(grouppquota.HardLimit)
601                        if softlimit <= val < hardlimit :   
602                            now = DateTime.now()
603                            if grouppquota.DateLimit is not None :
604                                datelimit = DateTime.ISO.ParseDateTime(str(grouppquota.DateLimit)[:19])
605                            else :
606                                datelimit = now + self.config.getGraceDelay(printer.Name)
607                                grouppquota.setDateLimit(datelimit)
608                            if now < datelimit :
609                                action = "WARN"
610                            else :   
611                                action = "DENY"
612                        else :         
613                            action = "DENY"
614            else :       
615                if grouppquota.HardLimit is not None :
616                    # no soft limit, only a hard one.
617                    hardlimit = int(grouppquota.HardLimit)
618                    if val < hardlimit :
619                        action = "ALLOW"
620                    else :     
621                        action = "DENY"
622                else :
623                    # Both are unset, no quota, i.e. accounting only
624                    action = "ALLOW"
625        return action
626   
627    def checkUserPQuota(self, userpquota) :
628        """Checks the user quota on a printer and all its parents and deny or accept the job."""
629        user = userpquota.User
630        printer = userpquota.Printer
631       
632        # indicates that a warning needs to be sent
633        warned = 0               
634       
635        # first we check any group the user is a member of
636        for group in self.storage.getUserGroups(user) :
637            # No need to check anything if the group is in noquota mode
638            if group.LimitBy != "noquota" :
639                grouppquota = self.storage.getGroupPQuota(group, printer)
640                # for the printer and all its parents
641                for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
642                    if gpquota.Exists :
643                        action = self.checkGroupPQuota(gpquota)
644                        if action == "DENY" :
645                            return action
646                        elif action == "WARN" :   
647                            warned = 1
648                       
649        # Then we check the user's account balance
650        # if we get there we are sure that policy is not EXTERNAL
651        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
652        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
653            self.logdebug("Checking account balance for user %s" % user.Name)
654            if user.AccountBalance is None :
655                if policy == "ALLOW" :
656                    action = "POLICY_ALLOW"
657                else :   
658                    action = "POLICY_DENY"
659                self.printInfo(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
660                return action       
661            else :   
662                if user.OverCharge == 0.0 :
663                    self.printInfo(_("User %s will not be charged for printing.") % user.Name)
664                    action = "ALLOW"
665                else :
666                    val = float(user.AccountBalance or 0.0)
667                    enforcement = self.config.getPrinterEnforcement(printer.Name)
668                    if enforcement == "STRICT" : 
669                        val -= self.softwareJobPrice # use precomputed size.
670                    balancezero = self.config.getBalanceZero()   
671                    if val <= balancezero :
672                        action = "DENY"
673                    elif val <= self.config.getPoorMan() :   
674                        action = "WARN"
675                    else :
676                        action = "ALLOW"
677                    if (enforcement == "STRICT") and (val == balancezero) :
678                        action = "WARN" # we can still print until account is 0
679                return action   
680        else :
681            # Then check the user quota on current printer and all its parents.               
682            policyallowed = 0
683            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
684                action = self._checkUserPQuota(upquota)
685                if action in ("DENY", "POLICY_DENY") :
686                    return action
687                elif action == "WARN" :   
688                    warned = 1
689                elif action == "POLICY_ALLOW" :   
690                    policyallowed = 1
691            if warned :       
692                return "WARN"
693            elif policyallowed :   
694                return "POLICY_ALLOW" 
695            else :   
696                return "ALLOW"
697               
698    def externalMailTo(self, cmd, action, user, printer, message) :
699        """Warns the user with an external command."""
700        username = user.Name
701        printername = printer.Name
702        email = user.Email or user.Name
703        if "@" not in email :
704            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
705        os.system(cmd % locals())
706   
707    def formatCommandLine(self, cmd, user, printer) :
708        """Executes an external command."""
709        username = user.Name
710        printername = printer.Name
711        return cmd % locals()
712       
713    def warnGroupPQuota(self, grouppquota) :
714        """Checks a group quota and send messages if quota is exceeded on current printer."""
715        group = grouppquota.Group
716        printer = grouppquota.Printer
717        admin = self.config.getAdmin(printer.Name)
718        adminmail = self.config.getAdminMail(printer.Name)
719        (mailto, arguments) = self.config.getMailTo(printer.Name)
720        if group.LimitBy in ("noquota", "nochange") :
721            action = "ALLOW"
722        else :   
723            action = self.checkGroupPQuota(grouppquota)
724            if action.startswith("POLICY_") :
725                action = action[7:]
726            if action == "DENY" :
727                adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
728                self.printInfo(adminmessage)
729                if mailto in [ "BOTH", "ADMIN" ] :
730                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
731                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
732                    for user in self.storage.getGroupMembers(group) :
733                        if mailto != "EXTERNAL" :
734                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
735                        else :   
736                            self.externalMailTo(arguments, action, user, printer, self.config.getHardWarn(printer.Name))
737            elif action == "WARN" :   
738                adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
739                self.printInfo(adminmessage)
740                if mailto in [ "BOTH", "ADMIN" ] :
741                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
742                if group.LimitBy and (group.LimitBy.lower() == "balance") : 
743                    message = self.config.getPoorWarn()
744                else :     
745                    message = self.config.getSoftWarn(printer.Name)
746                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
747                    for user in self.storage.getGroupMembers(group) :
748                        if mailto != "EXTERNAL" :
749                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
750                        else :   
751                            self.externalMailTo(arguments, action, user, printer, message)
752        return action       
753       
754    def warnUserPQuota(self, userpquota) :
755        """Checks a user quota and send him a message if quota is exceeded on current printer."""
756        user = userpquota.User
757        printer = userpquota.Printer
758        admin = self.config.getAdmin(printer.Name)
759        adminmail = self.config.getAdminMail(printer.Name)
760        (mailto, arguments) = self.config.getMailTo(printer.Name)
761       
762        if user.LimitBy in ("noquota", "nochange") :
763            action = "ALLOW"
764        elif user.LimitBy == "noprint" :
765            action = "DENY"
766            message = _("User %s is not allowed to print at this time.") % user.Name
767            self.printInfo(message)
768            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
769                if mailto != "EXTERNAL" :
770                    self.sendMessageToUser(admin, adminmail, user, _("Printing denied."), message)
771                else :   
772                    self.externalMailTo(arguments, action, user, printer, message)
773            if mailto in [ "BOTH", "ADMIN" ] :
774                self.sendMessageToAdmin(adminmail, _("Print Quota"), message)
775        else :
776            action = self.checkUserPQuota(userpquota)
777            if action.startswith("POLICY_") :
778                action = action[7:]
779               
780            if action == "DENY" :
781                adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
782                self.printInfo(adminmessage)
783                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
784                    message = self.config.getHardWarn(printer.Name)
785                    if mailto != "EXTERNAL" :
786                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
787                    else :   
788                        self.externalMailTo(arguments, action, user, printer, message)
789                if mailto in [ "BOTH", "ADMIN" ] :
790                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
791            elif action == "WARN" :   
792                adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
793                self.printInfo(adminmessage)
794                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
795                    if user.LimitBy and (user.LimitBy.lower() == "balance") : 
796                        message = self.config.getPoorWarn()
797                    else :     
798                        message = self.config.getSoftWarn(printer.Name)
799                    if mailto != "EXTERNAL" :   
800                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
801                    else :   
802                        self.externalMailTo(arguments, action, user, printer, message)
803                if mailto in [ "BOTH", "ADMIN" ] :
804                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
805        return action       
806       
Note: See TracBrowser for help on using the browser.