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

Revision 3173, 35.4 kB (checked in by jerome, 17 years ago)

Dirty hack of the day (year ?).

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