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

Revision 3006, 35.2 kB (checked in by jerome, 18 years ago)

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