Code

Finished implementation of session and one-time-key stores for RDBMS
[roundup.git] / roundup / cgi / actions.py
1 import re, cgi, StringIO, urllib, Cookie, time, random
3 from roundup import hyperdb, token, date, password, rcsv
4 from roundup.i18n import _
5 from roundup.cgi import templating
6 from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
7 from roundup.mailgw import uidFromAddress
9 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
10            'EditCSVAction', 'EditItemAction', 'PassResetAction',
11            'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
12            'NewItemAction']
14 # used by a couple of routines
15 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
17 class Action:
18     def __init__(self, client):
19         self.client = client
20         self.form = client.form
21         self.db = client.db
22         self.nodeid = client.nodeid
23         self.template = client.template
24         self.classname = client.classname
25         self.userid = client.userid
26         self.base = client.base
27         self.user = client.user
29     def execute(self):
30         """Execute the action specified by this object."""
31         self.permission()
32         self.handle()
34     name = ''
35     permissionType = None
36     def permission(self):
37         """Check whether the user has permission to execute this action.
39         True by default. If the permissionType attribute is a string containing
40         a simple permission, check whether the user has that permission.
41         Subclasses must also define the name attribute if they define
42         permissionType.
44         Despite having this permission, users may still be unauthorised to
45         perform parts of actions. It is up to the subclasses to detect this.
46         """
47         if (self.permissionType and
48                 not self.hasPermission(self.permissionType)):
49             info = {'action': self.name, 'classname': self.classname}
50             raise Unauthorised, _('You do not have permission to '
51                 '%(action)s the %(classname)s class.')%info
53     def hasPermission(self, permission):
54         """Check whether the user has 'permission' on the current class."""
55         return self.db.security.hasPermission(permission, self.client.userid,
56             self.client.classname)
58 class ShowAction(Action):
59     def handle(self, typere=re.compile('[@:]type'),
60                numre=re.compile('[@:]number')):
61         """Show a node of a particular class/id."""
62         t = n = ''
63         for key in self.form.keys():
64             if typere.match(key):
65                 t = self.form[key].value.strip()
66             elif numre.match(key):
67                 n = self.form[key].value.strip()
68         if not t:
69             raise ValueError, 'No type specified'
70         if not n:
71             raise SeriousError, _('No ID entered')
72         try:
73             int(n)
74         except ValueError:
75             d = {'input': n, 'classname': t}
76             raise SeriousError, _(
77                 '"%(input)s" is not an ID (%(classname)s ID required)')%d
78         url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
79         raise Redirect, url
81 class RetireAction(Action):
82     name = 'retire'
83     permissionType = 'Edit'
85     def handle(self):
86         """Retire the context item."""
87         # if we want to view the index template now, then unset the nodeid
88         # context info (a special-case for retire actions on the index page)
89         nodeid = self.nodeid
90         if self.template == 'index':
91             self.client.nodeid = None
93         # make sure we don't try to retire admin or anonymous
94         if self.classname == 'user' and \
95                 self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
96             raise ValueError, _('You may not retire the admin or anonymous user')
98         # do the retire
99         self.db.getclass(self.classname).retire(nodeid)
100         self.db.commit()
102         self.client.ok_message.append(
103             _('%(classname)s %(itemid)s has been retired')%{
104                 'classname': self.classname.capitalize(), 'itemid': nodeid})
106 class SearchAction(Action):
107     name = 'search'
108     permissionType = 'View'
110     def handle(self, wcre=re.compile(r'[\s,]+')):
111         """Mangle some of the form variables.
113         Set the form ":filter" variable based on the values of the filter
114         variables - if they're set to anything other than "dontcare" then add
115         them to :filter.
117         Handle the ":queryname" variable and save off the query to the user's
118         query list.
120         Split any String query values on whitespace and comma.
122         """
123         self.fakeFilterVars()
124         queryname = self.getQueryName()
126         # handle saving the query params
127         if queryname:
128             # parse the environment and figure what the query _is_
129             req = templating.HTMLRequest(self.client)
131             # The [1:] strips off the '?' character, it isn't part of the
132             # query string.
133             url = req.indexargs_href('', {})[1:]
135             # handle editing an existing query
136             try:
137                 qid = self.db.query.lookup(queryname)
138                 self.db.query.set(qid, klass=self.classname, url=url)
139             except KeyError:
140                 # create a query
141                 qid = self.db.query.create(name=queryname,
142                     klass=self.classname, url=url)
144             # and add it to the user's query multilink
145             queries = self.db.user.get(self.userid, 'queries')
146             if qid not in queries:
147                 queries.append(qid)
148                 self.db.user.set(self.userid, queries=queries)
150             # commit the query change to the database
151             self.db.commit()
153     def fakeFilterVars(self):
154         """Add a faked :filter form variable for each filtering prop."""
155         props = self.db.classes[self.classname].getprops()
156         for key in self.form.keys():
157             if not props.has_key(key):
158                 continue
159             if isinstance(self.form[key], type([])):
160                 # search for at least one entry which is not empty
161                 for minifield in self.form[key]:
162                     if minifield.value:
163                         break
164                 else:
165                     continue
166             else:
167                 if not self.form[key].value:
168                     continue
169                 if isinstance(props[key], hyperdb.String):
170                     v = self.form[key].value
171                     l = token.token_split(v)
172                     if len(l) > 1 or l[0] != v:
173                         self.form.value.remove(self.form[key])
174                         # replace the single value with the split list
175                         for v in l:
176                             self.form.value.append(cgi.MiniFieldStorage(key, v))
178             self.form.value.append(cgi.MiniFieldStorage('@filter', key))
180     FV_QUERYNAME = re.compile(r'[@:]queryname')
181     def getQueryName(self):
182         for key in self.form.keys():
183             if self.FV_QUERYNAME.match(key):
184                 return self.form[key].value.strip()
185         return ''
187 class EditCSVAction(Action):
188     name = 'edit'
189     permissionType = 'Edit'
191     def handle(self):
192         """Performs an edit of all of a class' items in one go.
194         The "rows" CGI var defines the CSV-formatted entries for the class. New
195         nodes are identified by the ID 'X' (or any other non-existent ID) and
196         removed lines are retired.
198         """
199         # get the CSV module
200         if rcsv.error:
201             self.client.error_message.append(_(rcsv.error))
202             return
204         cl = self.db.classes[self.classname]
205         idlessprops = cl.getprops(protected=0).keys()
206         idlessprops.sort()
207         props = ['id'] + idlessprops
209         # do the edit
210         rows = StringIO.StringIO(self.form['rows'].value)
211         reader = rcsv.reader(rows, rcsv.comma_separated)
212         found = {}
213         line = 0
214         for values in reader:
215             line += 1
216             if line == 1: continue
217             # skip property names header
218             if values == props:
219                 continue
221             # extract the nodeid
222             nodeid, values = values[0], values[1:]
223             found[nodeid] = 1
225             # see if the node exists
226             if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
227                 exists = 0
228             else:
229                 exists = 1
231             # confirm correct weight
232             if len(idlessprops) != len(values):
233                 self.client.error_message.append(
234                     _('Not enough values on line %(line)s')%{'line':line})
235                 return
237             # extract the new values
238             d = {}
239             for name, value in zip(idlessprops, values):
240                 prop = cl.properties[name]
241                 value = value.strip()
242                 # only add the property if it has a value
243                 if value:
244                     # if it's a multilink, split it
245                     if isinstance(prop, hyperdb.Multilink):
246                         value = value.split(':')
247                     elif isinstance(prop, hyperdb.Password):
248                         value = password.Password(value)
249                     elif isinstance(prop, hyperdb.Interval):
250                         value = date.Interval(value)
251                     elif isinstance(prop, hyperdb.Date):
252                         value = date.Date(value)
253                     elif isinstance(prop, hyperdb.Boolean):
254                         value = value.lower() in ('yes', 'true', 'on', '1')
255                     elif isinstance(prop, hyperdb.Number):
256                         value = float(value)
257                     d[name] = value
258                 elif exists:
259                     # nuke the existing value
260                     if isinstance(prop, hyperdb.Multilink):
261                         d[name] = []
262                     else:
263                         d[name] = None
265             # perform the edit
266             if exists:
267                 # edit existing
268                 cl.set(nodeid, **d)
269             else:
270                 # new node
271                 found[cl.create(**d)] = 1
273         # retire the removed entries
274         for nodeid in cl.list():
275             if not found.has_key(nodeid):
276                 cl.retire(nodeid)
278         # all OK
279         self.db.commit()
281         self.client.ok_message.append(_('Items edited OK'))
283 class _EditAction(Action):
284     def isEditingSelf(self):
285         """Check whether a user is editing his/her own details."""
286         return (self.nodeid == self.userid
287                 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
289     def editItemPermission(self, props):
290         """Determine whether the user has permission to edit this item.
292         Base behaviour is to check the user can edit this class. If we're
293         editing the "user" class, users are allowed to edit their own details.
294         Unless it's the "roles" property, which requires the special Permission
295         "Web Roles".
296         """
297         if self.classname == 'user':
298             if props.has_key('roles') and not self.hasPermission('Web Roles'):
299                 raise Unauthorised, _("You do not have permission to edit user roles")
300             if self.isEditingSelf():
301                 return 1
302         if self.hasPermission('Edit'):
303             return 1
304         return 0
306     def newItemPermission(self, props):
307         """Determine whether the user has permission to create (edit) this item.
309         Base behaviour is to check the user can edit this class. No additional
310         property checks are made. Additionally, new user items may be created
311         if the user has the "Web Registration" Permission.
313         """
314         if (self.classname == 'user' and self.hasPermission('Web Registration')
315             or self.hasPermission('Edit')):
316             return 1
317         return 0
319     #
320     #  Utility methods for editing
321     #
322     def _editnodes(self, all_props, all_links, newids=None):
323         ''' Use the props in all_props to perform edit and creation, then
324             use the link specs in all_links to do linking.
325         '''
326         # figure dependencies and re-work links
327         deps = {}
328         links = {}
329         for cn, nodeid, propname, vlist in all_links:
330             if not all_props.has_key((cn, nodeid)):
331                 # link item to link to doesn't (and won't) exist
332                 continue
333             for value in vlist:
334                 if not all_props.has_key(value):
335                     # link item to link to doesn't (and won't) exist
336                     continue
337                 deps.setdefault((cn, nodeid), []).append(value)
338                 links.setdefault(value, []).append((cn, nodeid, propname))
340         # figure chained dependencies ordering
341         order = []
342         done = {}
343         # loop detection
344         change = 0
345         while len(all_props) != len(done):
346             for needed in all_props.keys():
347                 if done.has_key(needed):
348                     continue
349                 tlist = deps.get(needed, [])
350                 for target in tlist:
351                     if not done.has_key(target):
352                         break
353                 else:
354                     done[needed] = 1
355                     order.append(needed)
356                     change = 1
357             if not change:
358                 raise ValueError, 'linking must not loop!'
360         # now, edit / create
361         m = []
362         for needed in order:
363             props = all_props[needed]
364             if not props:
365                 # nothing to do
366                 continue
367             cn, nodeid = needed
369             if nodeid is not None and int(nodeid) > 0:
370                 # make changes to the node
371                 props = self._changenode(cn, nodeid, props)
373                 # and some nice feedback for the user
374                 if props:
375                     info = ', '.join(props.keys())
376                     m.append('%s %s %s edited ok'%(cn, nodeid, info))
377                 else:
378                     m.append('%s %s - nothing changed'%(cn, nodeid))
379             else:
380                 assert props
382                 # make a new node
383                 newid = self._createnode(cn, props)
384                 if nodeid is None:
385                     self.nodeid = newid
386                 nodeid = newid
388                 # and some nice feedback for the user
389                 m.append('%s %s created'%(cn, newid))
391             # fill in new ids in links
392             if links.has_key(needed):
393                 for linkcn, linkid, linkprop in links[needed]:
394                     props = all_props[(linkcn, linkid)]
395                     cl = self.db.classes[linkcn]
396                     propdef = cl.getprops()[linkprop]
397                     if not props.has_key(linkprop):
398                         if linkid is None or linkid.startswith('-'):
399                             # linking to a new item
400                             if isinstance(propdef, hyperdb.Multilink):
401                                 props[linkprop] = [newid]
402                             else:
403                                 props[linkprop] = newid
404                         else:
405                             # linking to an existing item
406                             if isinstance(propdef, hyperdb.Multilink):
407                                 existing = cl.get(linkid, linkprop)[:]
408                                 existing.append(nodeid)
409                                 props[linkprop] = existing
410                             else:
411                                 props[linkprop] = newid
413         return '<br>'.join(m)
415     def _changenode(self, cn, nodeid, props):
416         """Change the node based on the contents of the form."""
417         # check for permission
418         if not self.editItemPermission(props):
419             raise Unauthorised, 'You do not have permission to edit %s'%cn
421         # make the changes
422         cl = self.db.classes[cn]
423         return cl.set(nodeid, **props)
425     def _createnode(self, cn, props):
426         """Create a node based on the contents of the form."""
427         # check for permission
428         if not self.newItemPermission(props):
429             raise Unauthorised, 'You do not have permission to create %s'%cn
431         # create the node and return its id
432         cl = self.db.classes[cn]
433         return cl.create(**props)
435 class EditItemAction(_EditAction):
436     def lastUserActivity(self):
437         if self.form.has_key(':lastactivity'):
438             return date.Date(self.form[':lastactivity'].value)
439         elif self.form.has_key('@lastactivity'):
440             return date.Date(self.form['@lastactivity'].value)
441         else:
442             return None
444     def lastNodeActivity(self):
445         cl = getattr(self.client.db, self.classname)
446         return cl.get(self.nodeid, 'activity')
448     def detectCollision(self, userActivity, nodeActivity):
449         # Result from lastUserActivity may be None. If it is, assume there's no
450         # conflict, or at least not one we can detect.
451         if userActivity:
452             return userActivity < nodeActivity
454     def handleCollision(self):
455         self.client.template = 'collision'
457     def handle(self):
458         """Perform an edit of an item in the database.
460         See parsePropsFromForm and _editnodes for special variables.
462         """
463         if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
464             self.handleCollision()
465             return
467         props, links = self.client.parsePropsFromForm()
469         # handle the props
470         try:
471             message = self._editnodes(props, links)
472         except (ValueError, KeyError, IndexError), message:
473             self.client.error_message.append(_('Apply Error: ') + str(message))
474             return
476         # commit now that all the tricky stuff is done
477         self.db.commit()
479         # redirect to the item's edit page
480         # redirect to finish off
481         url = self.base + self.classname
482         # note that this action might have been called by an index page, so
483         # we will want to include index-page args in this URL too
484         if self.nodeid is not None:
485             url += self.nodeid
486         url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
487             urllib.quote(self.template))
488         if self.nodeid is None:
489             req = templating.HTMLRequest(self)
490             url += '&' + req.indexargs_href('', {})[1:]
491         raise Redirect, url
493 class NewItemAction(_EditAction):
494     def handle(self):
495         ''' Add a new item to the database.
497             This follows the same form as the EditItemAction, with the same
498             special form values.
499         '''
500         # parse the props from the form
501         try:
502             props, links = self.client.parsePropsFromForm(create=True)
503         except (ValueError, KeyError), message:
504             self.client.error_message.append(_('Error: ') + str(message))
505             return
507         # handle the props - edit or create
508         try:
509             # when it hits the None element, it'll set self.nodeid
510             messages = self._editnodes(props, links)
512         except (ValueError, KeyError, IndexError), message:
513             # these errors might just be indicative of user dumbness
514             self.client.error_message.append(_('Error: ') + str(message))
515             return
517         # commit now that all the tricky stuff is done
518         self.db.commit()
520         # redirect to the new item's page
521         raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
522             self.classname, self.nodeid, urllib.quote(messages),
523             urllib.quote(self.template))
525 class PassResetAction(Action):
526     def handle(self):
527         """Handle password reset requests.
529         Presence of either "name" or "address" generates email. Presence of
530         "otk" performs the reset.
532         """
533         if self.form.has_key('otk'):
534             # pull the rego information out of the otk database
535             otk = self.form['otk'].value
536             otks = self.db.getOTKManager()
537             uid = otks.get(otk, 'uid')
538             if uid is None:
539                 self.client.error_message.append("""Invalid One Time Key!
540 (a Mozilla bug may cause this message to show up erroneously,
541  please check your email)""")
542                 return
544             # re-open the database as "admin"
545             if self.user != 'admin':
546                 self.client.opendb('admin')
547                 self.db = self.client.db
549             # change the password
550             newpw = password.generatePassword()
552             cl = self.db.user
553             # XXX we need to make the "default" page be able to display errors!
554             try:
555                 # set the password
556                 cl.set(uid, password=password.Password(newpw))
557                 # clear the props from the otk database
558                 otks.destroy(otk)
559                 self.db.commit()
560             except (ValueError, KeyError), message:
561                 self.client.error_message.append(str(message))
562                 return
564             # user info
565             address = self.db.user.get(uid, 'address')
566             name = self.db.user.get(uid, 'username')
568             # send the email
569             tracker_name = self.db.config.TRACKER_NAME
570             subject = 'Password reset for %s'%tracker_name
571             body = '''
572 The password has been reset for username "%(name)s".
574 Your password is now: %(password)s
575 '''%{'name': name, 'password': newpw}
576             if not self.client.standard_message([address], subject, body):
577                 return
579             self.client.ok_message.append(
580                     'Password reset and email sent to %s'%address)
581             return
583         # no OTK, so now figure the user
584         if self.form.has_key('username'):
585             name = self.form['username'].value
586             try:
587                 uid = self.db.user.lookup(name)
588             except KeyError:
589                 self.client.error_message.append('Unknown username')
590                 return
591             address = self.db.user.get(uid, 'address')
592         elif self.form.has_key('address'):
593             address = self.form['address'].value
594             uid = uidFromAddress(self.db, ('', address), create=0)
595             if not uid:
596                 self.client.error_message.append('Unknown email address')
597                 return
598             name = self.db.user.get(uid, 'username')
599         else:
600             self.client.error_message.append('You need to specify a username '
601                 'or address')
602             return
604         # generate the one-time-key and store the props for later
605         otk = ''.join([random.choice(chars) for x in range(32)])
606         while otks.exists(otk):
607             otk = ''.join([random.choice(chars) for x in range(32)])
608         otks.set(otk, uid=uid)
609         self.db.commit()
611         # send the email
612         tracker_name = self.db.config.TRACKER_NAME
613         subject = 'Confirm reset of password for %s'%tracker_name
614         body = '''
615 Someone, perhaps you, has requested that the password be changed for your
616 username, "%(name)s". If you wish to proceed with the change, please follow
617 the link below:
619   %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
621 You should then receive another email with the new password.
622 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
623         if not self.client.standard_message([address], subject, body):
624             return
626         self.client.ok_message.append('Email sent to %s'%address)
628 class ConfRegoAction(Action):
629     def handle(self):
630         """Grab the OTK, use it to load up the new user details."""
631         try:
632             # pull the rego information out of the otk database
633             self.userid = self.db.confirm_registration(self.form['otk'].value)
634         except (ValueError, KeyError), message:
635             self.client.error_message.append(str(message))
636             return
638         # log the new user in
639         self.client.user = self.db.user.get(self.userid, 'username')
640         # re-open the database for real, using the user
641         self.client.opendb(self.client.user)
643         # if we have a session, update it
644         if hasattr(self, 'session'):
645             self.client.db.sessions.set(self.session, user=self.user,
646                 last_use=time.time())
647         else:
648             # new session cookie
649             self.client.set_cookie(self.user)
651         # nice message
652         message = _('You are now registered, welcome!')
653         url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
654             urllib.quote(message))
656         # redirect to the user's page (but not 302, as some email clients seem
657         # to want to reload the page, or something)
658         return '''<html><head><title>%s</title></head>
659             <body><p><a href="%s">%s</a></p>
660             <script type="text/javascript">
661             window.setTimeout('window.location = "%s"', 1000);
662             </script>'''%(message, url, message, url)
664 class RegisterAction(Action):
665     name = 'register'
666     permissionType = 'Web Registration'
668     def handle(self):
669         """Attempt to create a new user based on the contents of the form
670         and then set the cookie.
672         Return 1 on successful login.
673         """
674         props = self.client.parsePropsFromForm(create=True)[0][('user', None)]
676         # registration isn't allowed to supply roles
677         if props.has_key('roles'):
678             raise Unauthorised, _("It is not permitted to supply roles "
679                 "at registration.")
681         username = props['username']
682         try:
683             self.db.user.lookup(username)
684             self.client.error_message.append(_('Error: A user with the '
685                 'username "%(username)s" already exists')%props)
686             return
687         except KeyError:
688             pass
690         # generate the one-time-key and store the props for later
691         for propname, proptype in self.db.user.getprops().items():
692             value = props.get(propname, None)
693             if value is None:
694                 pass
695             elif isinstance(proptype, hyperdb.Date):
696                 props[propname] = str(value)
697             elif isinstance(proptype, hyperdb.Interval):
698                 props[propname] = str(value)
699             elif isinstance(proptype, hyperdb.Password):
700                 props[propname] = str(value)
701         otks = self.db.getOTKManager()
702         while otks.exists(otk):
703             otk = ''.join([random.choice(chars) for x in range(32)])
704         otks.set(otk, **props)
706         # send the email
707         tracker_name = self.db.config.TRACKER_NAME
708         tracker_email = self.db.config.TRACKER_EMAIL
709         subject = 'Complete your registration to %s -- key %s'%(tracker_name,
710                                                                   otk)
711         body = """To complete your registration of the user "%(name)s" with
712 %(tracker)s, please do one of the following:
714 - send a reply to %(tracker_email)s and maintain the subject line as is (the
715 reply's additional "Re:" is ok),
717 - or visit the following URL:
719 %(url)s?@action=confrego&otk=%(otk)s
720 """ % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
721         'otk': otk, 'tracker_email': tracker_email}
722         if not self.client.standard_message([props['address']], subject, body,
723         tracker_email):
724             return
726         # commit changes to the database
727         self.db.commit()
729         # redirect to the "you're almost there" page
730         raise Redirect, '%suser?@template=rego_progress'%self.base
732 class LogoutAction(Action):
733     def handle(self):
734         """Make us really anonymous - nuke the cookie too."""
735         # log us out
736         self.client.make_user_anonymous()
738         # construct the logout cookie
739         now = Cookie._getdate()
740         self.client.additional_headers['Set-Cookie'] = \
741            '%s=deleted; Max-Age=0; expires=%s; Path=%s;'%(self.client.cookie_name,
742             now, self.client.cookie_path)
744         # Let the user know what's going on
745         self.client.ok_message.append(_('You are logged out'))
747 class LoginAction(Action):
748     def handle(self):
749         """Attempt to log a user in.
751         Sets up a session for the user which contains the login credentials.
753         """
754         # we need the username at a minimum
755         if not self.form.has_key('__login_name'):
756             self.client.error_message.append(_('Username required'))
757             return
759         # get the login info
760         self.client.user = self.form['__login_name'].value
761         if self.form.has_key('__login_password'):
762             password = self.form['__login_password'].value
763         else:
764             password = ''
766         # make sure the user exists
767         try:
768             self.client.userid = self.db.user.lookup(self.client.user)
769         except KeyError:
770             name = self.client.user
771             self.client.error_message.append(_('No such user "%(name)s"')%locals())
772             self.client.make_user_anonymous()
773             return
775         # verify the password
776         if not self.verifyPassword(self.client.userid, password):
777             self.client.make_user_anonymous()
778             self.client.error_message.append(_('Incorrect password'))
779             return
781         # Determine whether the user has permission to log in.
782         # Base behaviour is to check the user has "Web Access".
783         if not self.hasPermission("Web Access"):
784             self.client.make_user_anonymous()
785             self.client.error_message.append(_("You do not have permission to login"))
786             return
788         # now we're OK, re-open the database for real, using the user
789         self.client.opendb(self.client.user)
791         # set the session cookie
792         self.client.set_cookie(self.client.user)
794     def verifyPassword(self, userid, password):
795         ''' Verify the password that the user has supplied
796         '''
797         stored = self.db.user.get(self.client.userid, 'password')
798         if password == stored:
799             return 1
800         if not password and not stored:
801             return 1
802         return 0