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

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

Fixed date and time parsing, although I was unable to reproduce the problem reported...

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