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

Revision 3187, 35.6 kB (checked in by jerome, 17 years ago)

Ensure that debug is on if PyKota fails before being able to read
the configuration file (permission problem)

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