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

Revision 3260, 35.7 kB (checked in by jerome, 16 years ago)

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

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