Code

fix issue2550502
[roundup.git] / roundup / cgi / actions.py
1 #$Id: actions.py,v 1.73 2008-08-18 05:04:01 richard Exp $
3 import re, cgi, StringIO, urllib, time, random, csv, codecs
5 from roundup import hyperdb, token, date, password
6 from roundup.i18n import _
7 import roundup.exceptions
8 from roundup.cgi import exceptions, templating
9 from roundup.mailgw import uidFromAddress
11 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
12            'EditCSVAction', 'EditItemAction', 'PassResetAction',
13            'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
14            'NewItemAction', 'ExportCSVAction']
16 # used by a couple of routines
17 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
19 class Action:
20     def __init__(self, client):
21         self.client = client
22         self.form = client.form
23         self.db = client.db
24         self.nodeid = client.nodeid
25         self.template = client.template
26         self.classname = client.classname
27         self.userid = client.userid
28         self.base = client.base
29         self.user = client.user
30         self.context = templating.context(client)
32     def handle(self):
33         """Action handler procedure"""
34         raise NotImplementedError
36     def execute(self):
37         """Execute the action specified by this object."""
38         self.permission()
39         return self.handle()
41     name = ''
42     permissionType = None
43     def permission(self):
44         """Check whether the user has permission to execute this action.
46         True by default. If the permissionType attribute is a string containing
47         a simple permission, check whether the user has that permission.
48         Subclasses must also define the name attribute if they define
49         permissionType.
51         Despite having this permission, users may still be unauthorised to
52         perform parts of actions. It is up to the subclasses to detect this.
53         """
54         if (self.permissionType and
55                 not self.hasPermission(self.permissionType)):
56             info = {'action': self.name, 'classname': self.classname}
57             raise exceptions.Unauthorised, self._(
58                 'You do not have permission to '
59                 '%(action)s the %(classname)s class.')%info
61     _marker = []
62     def hasPermission(self, permission, classname=_marker, itemid=None, property=None):
63         """Check whether the user has 'permission' on the current class."""
64         if classname is self._marker:
65             classname = self.client.classname
66         return self.db.security.hasPermission(permission, self.client.userid,
67             classname=classname, itemid=itemid, property=property)
69     def gettext(self, msgid):
70         """Return the localized translation of msgid"""
71         return self.client.translator.gettext(msgid)
73     _ = gettext
75 class ShowAction(Action):
77     typere=re.compile('[@:]type')
78     numre=re.compile('[@:]number')
80     def handle(self):
81         """Show a node of a particular class/id."""
82         t = n = ''
83         for key in self.form.keys():
84             if self.typere.match(key):
85                 t = self.form[key].value.strip()
86             elif self.numre.match(key):
87                 n = self.form[key].value.strip()
88         if not t:
89             raise ValueError, self._('No type specified')
90         if not n:
91             raise exceptions.SeriousError, self._('No ID entered')
92         try:
93             int(n)
94         except ValueError:
95             d = {'input': n, 'classname': t}
96             raise exceptions.SeriousError, self._(
97                 '"%(input)s" is not an ID (%(classname)s ID required)')%d
98         url = '%s%s%s'%(self.base, t, n)
99         raise exceptions.Redirect, url
101 class RetireAction(Action):
102     name = 'retire'
103     permissionType = 'Edit'
105     def handle(self):
106         """Retire the context item."""
107         # if we want to view the index template now, then unset the nodeid
108         # context info (a special-case for retire actions on the index page)
109         nodeid = self.nodeid
110         if self.template == 'index':
111             self.client.nodeid = None
113         # make sure we don't try to retire admin or anonymous
114         if self.classname == 'user' and \
115                 self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
116             raise ValueError, self._(
117                 'You may not retire the admin or anonymous user')
119         # do the retire
120         self.db.getclass(self.classname).retire(nodeid)
121         self.db.commit()
123         self.client.ok_message.append(
124             self._('%(classname)s %(itemid)s has been retired')%{
125                 'classname': self.classname.capitalize(), 'itemid': nodeid})
127     def hasPermission(self, permission, classname=Action._marker, itemid=None):
128         if itemid is None:
129             itemid = self.nodeid
130         return Action.hasPermission(self, permission, classname, itemid)
132 class SearchAction(Action):
133     name = 'search'
134     permissionType = 'View'
136     def handle(self):
137         """Mangle some of the form variables.
139         Set the form ":filter" variable based on the values of the filter
140         variables - if they're set to anything other than "dontcare" then add
141         them to :filter.
143         Handle the ":queryname" variable and save off the query to the user's
144         query list.
146         Split any String query values on whitespace and comma.
148         """
149         self.fakeFilterVars()
150         queryname = self.getQueryName()
152         # editing existing query name?
153         old_queryname = self.getFromForm('old-queryname')
155         # handle saving the query params
156         if queryname:
157             # parse the environment and figure what the query _is_
158             req = templating.HTMLRequest(self.client)
160             url = self.getCurrentURL(req)
162             key = self.db.query.getkey()
163             if key:
164                 # edit the old way, only one query per name
165                 try:
166                     qid = self.db.query.lookup(old_queryname)
167                     if not self.hasPermission('Edit', 'query', itemid=qid):
168                         raise exceptions.Unauthorised, self._(
169                             "You do not have permission to edit queries")
170                     self.db.query.set(qid, klass=self.classname, url=url)
171                 except KeyError:
172                     # create a query
173                     if not self.hasPermission('Create', 'query'):
174                         raise exceptions.Unauthorised, self._(
175                             "You do not have permission to store queries")
176                     qid = self.db.query.create(name=queryname,
177                         klass=self.classname, url=url)
178             else:
179                 # edit the new way, query name not a key any more
180                 # see if we match an existing private query
181                 uid = self.db.getuid()
182                 qids = self.db.query.filter(None, {'name': old_queryname,
183                         'private_for': uid})
184                 if not qids:
185                     # ok, so there's not a private query for the current user
186                     # - see if there's one created by them
187                     qids = self.db.query.filter(None, {'name': old_queryname,
188                         'creator': uid})
190                 if qids and old_queryname:
191                     # edit query - make sure we get an exact match on the name
192                     for qid in qids:
193                         if old_queryname != self.db.query.get(qid, 'name'):
194                             continue
195                         if not self.hasPermission('Edit', 'query', itemid=qid):
196                             raise exceptions.Unauthorised, self._(
197                             "You do not have permission to edit queries")
198                         self.db.query.set(qid, klass=self.classname,
199                             url=url, name=queryname)
200                 else:
201                     # create a query
202                     if not self.hasPermission('Create', 'query'):
203                         raise exceptions.Unauthorised, self._(
204                             "You do not have permission to store queries")
205                     qid = self.db.query.create(name=queryname,
206                         klass=self.classname, url=url, private_for=uid)
208             # and add it to the user's query multilink
209             queries = self.db.user.get(self.userid, 'queries')
210             if qid not in queries:
211                 queries.append(qid)
212                 self.db.user.set(self.userid, queries=queries)
214             # commit the query change to the database
215             self.db.commit()
217     def fakeFilterVars(self):
218         """Add a faked :filter form variable for each filtering prop."""
219         cls = self.db.classes[self.classname]
220         for key in self.form.keys():
221             prop = cls.get_transitive_prop(key)
222             if not prop:
223                 continue
224             if isinstance(self.form[key], type([])):
225                 # search for at least one entry which is not empty
226                 for minifield in self.form[key]:
227                     if minifield.value:
228                         break
229                 else:
230                     continue
231             else:
232                 if not self.form[key].value:
233                     continue
234                 if isinstance(prop, hyperdb.String):
235                     v = self.form[key].value
236                     l = token.token_split(v)
237                     if len(l) > 1 or l[0] != v:
238                         self.form.value.remove(self.form[key])
239                         # replace the single value with the split list
240                         for v in l:
241                             self.form.value.append(cgi.MiniFieldStorage(key, v))
243             self.form.value.append(cgi.MiniFieldStorage('@filter', key))
245     def getCurrentURL(self, req):
246         """Get current URL for storing as a query.
248         Note: We are removing the first character from the current URL,
249         because the leading '?' is not part of the query string.
251         Implementation note:
252         But maybe the template should be part of the stored query:
253         template = self.getFromForm('template')
254         if template:
255             return req.indexargs_url('', {'@template' : template})[1:]
256         """
257         return req.indexargs_url('', {})[1:]
259     def getFromForm(self, name):
260         for key in ('@' + name, ':' + name):
261             if self.form.has_key(key):
262                 return self.form[key].value.strip()
263         return ''
265     def getQueryName(self):
266         return self.getFromForm('queryname')
268 class EditCSVAction(Action):
269     name = 'edit'
270     permissionType = 'Edit'
272     def handle(self):
273         """Performs an edit of all of a class' items in one go.
275         The "rows" CGI var defines the CSV-formatted entries for the class. New
276         nodes are identified by the ID 'X' (or any other non-existent ID) and
277         removed lines are retired.
279         """
280         cl = self.db.classes[self.classname]
281         idlessprops = cl.getprops(protected=0).keys()
282         idlessprops.sort()
283         props = ['id'] + idlessprops
285         # do the edit
286         rows = StringIO.StringIO(self.form['rows'].value)
287         reader = csv.reader(rows)
288         found = {}
289         line = 0
290         for values in reader:
291             line += 1
292             if line == 1: continue
293             # skip property names header
294             if values == props:
295                 continue
297             # extract the nodeid
298             nodeid, values = values[0], values[1:]
299             found[nodeid] = 1
301             # see if the node exists
302             if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
303                 exists = 0
304             else:
305                 exists = 1
307             # confirm correct weight
308             if len(idlessprops) != len(values):
309                 self.client.error_message.append(
310                     self._('Not enough values on line %(line)s')%{'line':line})
311                 return
313             # extract the new values
314             d = {}
315             for name, value in zip(idlessprops, values):
316                 prop = cl.properties[name]
317                 value = value.strip()
318                 # only add the property if it has a value
319                 if value:
320                     # if it's a multilink, split it
321                     if isinstance(prop, hyperdb.Multilink):
322                         value = value.split(':')
323                     elif isinstance(prop, hyperdb.Password):
324                         value = password.Password(value)
325                     elif isinstance(prop, hyperdb.Interval):
326                         value = date.Interval(value)
327                     elif isinstance(prop, hyperdb.Date):
328                         value = date.Date(value)
329                     elif isinstance(prop, hyperdb.Boolean):
330                         value = value.lower() in ('yes', 'true', 'on', '1')
331                     elif isinstance(prop, hyperdb.Number):
332                         value = float(value)
333                     d[name] = value
334                 elif exists:
335                     # nuke the existing value
336                     if isinstance(prop, hyperdb.Multilink):
337                         d[name] = []
338                     else:
339                         d[name] = None
341             # perform the edit
342             if exists:
343                 # edit existing
344                 cl.set(nodeid, **d)
345             else:
346                 # new node
347                 found[cl.create(**d)] = 1
349         # retire the removed entries
350         for nodeid in cl.list():
351             if not found.has_key(nodeid):
352                 cl.retire(nodeid)
354         # all OK
355         self.db.commit()
357         self.client.ok_message.append(self._('Items edited OK'))
359 class EditCommon(Action):
360     '''Utility methods for editing.'''
362     def _editnodes(self, all_props, all_links):
363         ''' Use the props in all_props to perform edit and creation, then
364             use the link specs in all_links to do linking.
365         '''
366         # figure dependencies and re-work links
367         deps = {}
368         links = {}
369         for cn, nodeid, propname, vlist in all_links:
370             numeric_id = int (nodeid or 0)
371             if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
372                 # link item to link to doesn't (and won't) exist
373                 continue
375             for value in vlist:
376                 if not all_props.has_key(value):
377                     # link item to link to doesn't (and won't) exist
378                     continue
379                 deps.setdefault((cn, nodeid), []).append(value)
380                 links.setdefault(value, []).append((cn, nodeid, propname))
382         # figure chained dependencies ordering
383         order = []
384         done = {}
385         # loop detection
386         change = 0
387         while len(all_props) != len(done):
388             for needed in all_props.keys():
389                 if done.has_key(needed):
390                     continue
391                 tlist = deps.get(needed, [])
392                 for target in tlist:
393                     if not done.has_key(target):
394                         break
395                 else:
396                     done[needed] = 1
397                     order.append(needed)
398                     change = 1
399             if not change:
400                 raise ValueError, 'linking must not loop!'
402         # now, edit / create
403         m = []
404         for needed in order:
405             props = all_props[needed]
406             cn, nodeid = needed
407             if props:
408                 if nodeid is not None and int(nodeid) > 0:
409                     # make changes to the node
410                     props = self._changenode(cn, nodeid, props)
412                     # and some nice feedback for the user
413                     if props:
414                         info = ', '.join(map(self._, props.keys()))
415                         m.append(
416                             self._('%(class)s %(id)s %(properties)s edited ok')
417                             % {'class':cn, 'id':nodeid, 'properties':info})
418                     else:
419                         m.append(self._('%(class)s %(id)s - nothing changed')
420                             % {'class':cn, 'id':nodeid})
421                 else:
422                     assert props
424                     # make a new node
425                     newid = self._createnode(cn, props)
426                     if nodeid is None:
427                         self.nodeid = newid
428                     nodeid = newid
430                     # and some nice feedback for the user
431                     m.append(self._('%(class)s %(id)s created')
432                         % {'class':cn, 'id':newid})
434             # fill in new ids in links
435             if links.has_key(needed):
436                 for linkcn, linkid, linkprop in links[needed]:
437                     props = all_props[(linkcn, linkid)]
438                     cl = self.db.classes[linkcn]
439                     propdef = cl.getprops()[linkprop]
440                     if not props.has_key(linkprop):
441                         if linkid is None or linkid.startswith('-'):
442                             # linking to a new item
443                             if isinstance(propdef, hyperdb.Multilink):
444                                 props[linkprop] = [newid]
445                             else:
446                                 props[linkprop] = newid
447                         else:
448                             # linking to an existing item
449                             if isinstance(propdef, hyperdb.Multilink):
450                                 existing = cl.get(linkid, linkprop)[:]
451                                 existing.append(nodeid)
452                                 props[linkprop] = existing
453                             else:
454                                 props[linkprop] = newid
456         return '<br>'.join(m)
458     def _changenode(self, cn, nodeid, props):
459         """Change the node based on the contents of the form."""
460         # check for permission
461         if not self.editItemPermission(props, classname=cn, itemid=nodeid):
462             raise exceptions.Unauthorised, self._(
463                 'You do not have permission to edit %(class)s'
464             ) % {'class': cn}
466         # make the changes
467         cl = self.db.classes[cn]
468         return cl.set(nodeid, **props)
470     def _createnode(self, cn, props):
471         """Create a node based on the contents of the form."""
472         # check for permission
473         if not self.newItemPermission(props, classname=cn):
474             raise exceptions.Unauthorised, self._(
475                 'You do not have permission to create %(class)s'
476             ) % {'class': cn}
478         # create the node and return its id
479         cl = self.db.classes[cn]
480         return cl.create(**props)
482     def isEditingSelf(self):
483         """Check whether a user is editing his/her own details."""
484         return (self.nodeid == self.userid
485                 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
487     _cn_marker = []
488     def editItemPermission(self, props, classname=_cn_marker, itemid=None):
489         """Determine whether the user has permission to edit this item."""
490         if itemid is None:
491             itemid = self.nodeid
492         if classname is self._cn_marker:
493             classname = self.classname
494         # The user must have permission to edit each of the properties
495         # being changed.
496         for p in props:
497             if not self.hasPermission('Edit',
498                                       itemid=itemid,
499                                       classname=classname,
500                                       property=p):
501                 return 0
502         # Since the user has permission to edit all of the properties,
503         # the edit is OK.
504         return 1
506     def newItemPermission(self, props, classname=None):
507         """Determine whether the user has permission to create this item.
509         Base behaviour is to check the user can edit this class. No additional
510         property checks are made.
511         """
512         if not classname :
513             classname = self.client.classname
514         return self.hasPermission('Create', classname=classname)
516 class EditItemAction(EditCommon):
517     def lastUserActivity(self):
518         if self.form.has_key(':lastactivity'):
519             d = date.Date(self.form[':lastactivity'].value)
520         elif self.form.has_key('@lastactivity'):
521             d = date.Date(self.form['@lastactivity'].value)
522         else:
523             return None
524         d.second = int(d.second)
525         return d
527     def lastNodeActivity(self):
528         cl = getattr(self.client.db, self.classname)
529         activity = cl.get(self.nodeid, 'activity').local(0)
530         activity.second = int(activity.second)
531         return activity
533     def detectCollision(self, user_activity, node_activity):
534         '''Check for a collision and return the list of props we edited
535         that conflict.'''
536         if user_activity and user_activity < node_activity:
537             props, links = self.client.parsePropsFromForm()
538             key = (self.classname, self.nodeid)
539             # we really only collide for direct prop edit conflicts
540             return props[key].keys()
541         else:
542             return []
544     def handleCollision(self, props):
545         message = self._('Edit Error: someone else has edited this %s (%s). '
546             'View <a target="new" href="%s%s">their changes</a> '
547             'in a new window.')%(self.classname, ', '.join(props),
548             self.classname, self.nodeid)
549         self.client.error_message.append(message)
550         return
552     def handle(self):
553         """Perform an edit of an item in the database.
555         See parsePropsFromForm and _editnodes for special variables.
557         """
558         user_activity = self.lastUserActivity()
559         if user_activity:
560             props = self.detectCollision(user_activity, self.lastNodeActivity())
561             if props:
562                 self.handleCollision(props)
563                 return
565         props, links = self.client.parsePropsFromForm()
567         # handle the props
568         try:
569             message = self._editnodes(props, links)
570         except (ValueError, KeyError, IndexError,
571                 roundup.exceptions.Reject), message:
572             self.client.error_message.append(
573                 self._('Edit Error: %s') % str(message))
574             return
576         # commit now that all the tricky stuff is done
577         self.db.commit()
579         # redirect to the item's edit page
580         # redirect to finish off
581         url = self.base + self.classname
582         # note that this action might have been called by an index page, so
583         # we will want to include index-page args in this URL too
584         if self.nodeid is not None:
585             url += self.nodeid
586         url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
587             urllib.quote(self.template))
588         if self.nodeid is None:
589             req = templating.HTMLRequest(self.client)
590             url += '&' + req.indexargs_url('', {})[1:]
591         raise exceptions.Redirect, url
593 class NewItemAction(EditCommon):
594     def handle(self):
595         ''' Add a new item to the database.
597             This follows the same form as the EditItemAction, with the same
598             special form values.
599         '''
600         # parse the props from the form
601         try:
602             props, links = self.client.parsePropsFromForm(create=1)
603         except (ValueError, KeyError), message:
604             self.client.error_message.append(self._('Error: %s')
605                 % str(message))
606             return
608         # handle the props - edit or create
609         try:
610             # when it hits the None element, it'll set self.nodeid
611             messages = self._editnodes(props, links)
612         except (ValueError, KeyError, IndexError,
613                 roundup.exceptions.Reject), message:
614             # these errors might just be indicative of user dumbness
615             self.client.error_message.append(_('Error: %s') % str(message))
616             return
618         # commit now that all the tricky stuff is done
619         self.db.commit()
621         # redirect to the new item's page
622         raise exceptions.Redirect, '%s%s%s?@ok_message=%s&@template=%s' % (
623             self.base, self.classname, self.nodeid, urllib.quote(messages),
624             urllib.quote(self.template))
626 class PassResetAction(Action):
627     def handle(self):
628         """Handle password reset requests.
630         Presence of either "name" or "address" generates email. Presence of
631         "otk" performs the reset.
633         """
634         otks = self.db.getOTKManager()
635         if self.form.has_key('otk'):
636             # pull the rego information out of the otk database
637             otk = self.form['otk'].value
638             uid = otks.get(otk, 'uid', default=None)
639             if uid is None:
640                 self.client.error_message.append(
641                     self._("Invalid One Time Key!\n"
642                         "(a Mozilla bug may cause this message "
643                         "to show up erroneously, please check your email)"))
644                 return
646             # re-open the database as "admin"
647             if self.user != 'admin':
648                 self.client.opendb('admin')
649                 self.db = self.client.db
650                 otks = self.db.getOTKManager()
652             # change the password
653             newpw = password.generatePassword()
655             cl = self.db.user
656             # XXX we need to make the "default" page be able to display errors!
657             try:
658                 # set the password
659                 cl.set(uid, password=password.Password(newpw))
660                 # clear the props from the otk database
661                 otks.destroy(otk)
662                 self.db.commit()
663             except (ValueError, KeyError), message:
664                 self.client.error_message.append(str(message))
665                 return
667             # user info
668             address = self.db.user.get(uid, 'address')
669             name = self.db.user.get(uid, 'username')
671             # send the email
672             tracker_name = self.db.config.TRACKER_NAME
673             subject = 'Password reset for %s'%tracker_name
674             body = '''
675 The password has been reset for username "%(name)s".
677 Your password is now: %(password)s
678 '''%{'name': name, 'password': newpw}
679             if not self.client.standard_message([address], subject, body):
680                 return
682             self.client.ok_message.append(
683                 self._('Password reset and email sent to %s') % address)
684             return
686         # no OTK, so now figure the user
687         if self.form.has_key('username'):
688             name = self.form['username'].value
689             try:
690                 uid = self.db.user.lookup(name)
691             except KeyError:
692                 self.client.error_message.append(self._('Unknown username'))
693                 return
694             address = self.db.user.get(uid, 'address')
695         elif self.form.has_key('address'):
696             address = self.form['address'].value
697             uid = uidFromAddress(self.db, ('', address), create=0)
698             if not uid:
699                 self.client.error_message.append(
700                     self._('Unknown email address'))
701                 return
702             name = self.db.user.get(uid, 'username')
703         else:
704             self.client.error_message.append(
705                 self._('You need to specify a username or address'))
706             return
708         # generate the one-time-key and store the props for later
709         otk = ''.join([random.choice(chars) for x in range(32)])
710         while otks.exists(otk):
711             otk = ''.join([random.choice(chars) for x in range(32)])
712         otks.set(otk, uid=uid)
713         self.db.commit()
715         # send the email
716         tracker_name = self.db.config.TRACKER_NAME
717         subject = 'Confirm reset of password for %s'%tracker_name
718         body = '''
719 Someone, perhaps you, has requested that the password be changed for your
720 username, "%(name)s". If you wish to proceed with the change, please follow
721 the link below:
723   %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
725 You should then receive another email with the new password.
726 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
727         if not self.client.standard_message([address], subject, body):
728             return
730         self.client.ok_message.append(self._('Email sent to %s') % address)
732 class RegoCommon(Action):
733     def finishRego(self):
734         # log the new user in
735         self.client.userid = self.userid
736         user = self.client.user = self.db.user.get(self.userid, 'username')
737         # re-open the database for real, using the user
738         self.client.opendb(user)
740         # update session data
741         self.client.session_api.set(user=user)
743         # nice message
744         message = self._('You are now registered, welcome!')
745         url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
746             urllib.quote(message))
748         # redirect to the user's page (but not 302, as some email clients seem
749         # to want to reload the page, or something)
750         return '''<html><head><title>%s</title></head>
751             <body><p><a href="%s">%s</a></p>
752             <script type="text/javascript">
753             window.setTimeout('window.location = "%s"', 1000);
754             </script>'''%(message, url, message, url)
756 class ConfRegoAction(RegoCommon):
757     def handle(self):
758         """Grab the OTK, use it to load up the new user details."""
759         try:
760             # pull the rego information out of the otk database
761             self.userid = self.db.confirm_registration(self.form['otk'].value)
762         except (ValueError, KeyError), message:
763             self.client.error_message.append(str(message))
764             return
765         return self.finishRego()
767 class RegisterAction(RegoCommon, EditCommon):
768     name = 'register'
769     permissionType = 'Create'
771     def handle(self):
772         """Attempt to create a new user based on the contents of the form
773         and then remember it in session.
775         Return 1 on successful login.
776         """
777         # parse the props from the form
778         try:
779             props, links = self.client.parsePropsFromForm(create=1)
780         except (ValueError, KeyError), message:
781             self.client.error_message.append(self._('Error: %s')
782                 % str(message))
783             return
785         # registration isn't allowed to supply roles
786         user_props = props[('user', None)]
787         if user_props.has_key('roles'):
788             raise exceptions.Unauthorised, self._(
789                 "It is not permitted to supply roles at registration.")
791         # skip the confirmation step?
792         if self.db.config['INSTANT_REGISTRATION']:
793             # handle the create now
794             try:
795                 # when it hits the None element, it'll set self.nodeid
796                 messages = self._editnodes(props, links)
797             except (ValueError, KeyError, IndexError,
798                     roundup.exceptions.Reject), message:
799                 # these errors might just be indicative of user dumbness
800                 self.client.error_message.append(_('Error: %s') % str(message))
801                 return
803             # fix up the initial roles
804             self.db.user.set(self.nodeid,
805                 roles=self.db.config['NEW_WEB_USER_ROLES'])
807             # commit now that all the tricky stuff is done
808             self.db.commit()
810             # finish off by logging the user in
811             self.userid = self.nodeid
812             return self.finishRego()
814         # generate the one-time-key and store the props for later
815         for propname, proptype in self.db.user.getprops().items():
816             value = user_props.get(propname, None)
817             if value is None:
818                 pass
819             elif isinstance(proptype, hyperdb.Date):
820                 user_props[propname] = str(value)
821             elif isinstance(proptype, hyperdb.Interval):
822                 user_props[propname] = str(value)
823             elif isinstance(proptype, hyperdb.Password):
824                 user_props[propname] = str(value)
825         otks = self.db.getOTKManager()
826         otk = ''.join([random.choice(chars) for x in range(32)])
827         while otks.exists(otk):
828             otk = ''.join([random.choice(chars) for x in range(32)])
829         otks.set(otk, **user_props)
831         # send the email
832         tracker_name = self.db.config.TRACKER_NAME
833         tracker_email = self.db.config.TRACKER_EMAIL
834         if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
835             subject = 'Complete your registration to %s -- key %s'%(tracker_name,
836                                                                   otk)
837             body = """To complete your registration of the user "%(name)s" with
838 %(tracker)s, please do one of the following:
840 - send a reply to %(tracker_email)s and maintain the subject line as is (the
841 reply's additional "Re:" is ok),
843 - or visit the following URL:
845 %(url)s?@action=confrego&otk=%(otk)s
847 """ % {'name': user_props['username'], 'tracker': tracker_name,
848         'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
849         else:
850             subject = 'Complete your registration to %s'%(tracker_name)
851             body = """To complete your registration of the user "%(name)s" with
852 %(tracker)s, please visit the following URL:
854 %(url)s?@action=confrego&otk=%(otk)s
856 """ % {'name': user_props['username'], 'tracker': tracker_name,
857         'url': self.base, 'otk': otk}
858         if not self.client.standard_message([user_props['address']], subject,
859                 body, (tracker_name, tracker_email)):
860             return
862         # commit changes to the database
863         self.db.commit()
865         # redirect to the "you're almost there" page
866         raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
868 class LogoutAction(Action):
869     def handle(self):
870         """Make us really anonymous - nuke the session too."""
871         # log us out
872         self.client.make_user_anonymous()
873         self.client.session_api.destroy()
875         # Let the user know what's going on
876         self.client.ok_message.append(self._('You are logged out'))
878         # reset client context to render tracker home page
879         # instead of last viewed page (may be inaccessibe for anonymous)
880         self.client.classname = None
881         self.client.nodeid = None
882         self.client.template = None
884 class LoginAction(Action):
885     def handle(self):
886         """Attempt to log a user in.
888         Sets up a session for the user which contains the login credentials.
890         """
891         # we need the username at a minimum
892         if not self.form.has_key('__login_name'):
893             self.client.error_message.append(self._('Username required'))
894             return
896         # get the login info
897         self.client.user = self.form['__login_name'].value
898         if self.form.has_key('__login_password'):
899             password = self.form['__login_password'].value
900         else:
901             password = ''
903         try:
904             self.verifyLogin(self.client.user, password)
905         except exceptions.LoginError, err:
906             self.client.make_user_anonymous()
907             self.client.error_message.extend(list(err.args))
908             return
910         # now we're OK, re-open the database for real, using the user
911         self.client.opendb(self.client.user)
913         # save user in session
914         self.client.session_api.set(user=self.client.user)
915         if self.form.has_key('remember'):
916             self.client.session_api.update(set_cookie=True, expire=24*3600*365)
918         # If we came from someplace, go back there
919         if self.form.has_key('__came_from'):
920             raise exceptions.Redirect, self.form['__came_from'].value
922     def verifyLogin(self, username, password):
923         # make sure the user exists
924         try:
925             self.client.userid = self.db.user.lookup(username)
926         except KeyError:
927             raise exceptions.LoginError, self._('Invalid login')
929         # verify the password
930         if not self.verifyPassword(self.client.userid, password):
931             raise exceptions.LoginError, self._('Invalid login')
933         # Determine whether the user has permission to log in.
934         # Base behaviour is to check the user has "Web Access".
935         if not self.hasPermission("Web Access"):
936             raise exceptions.LoginError, self._(
937                 "You do not have permission to login")
939     def verifyPassword(self, userid, password):
940         '''Verify the password that the user has supplied'''
941         stored = self.db.user.get(userid, 'password')
942         if password == stored:
943             return 1
944         if not password and not stored:
945             return 1
946         return 0
948 class ExportCSVAction(Action):
949     name = 'export'
950     permissionType = 'View'
952     def handle(self):
953         ''' Export the specified search query as CSV. '''
954         # figure the request
955         request = templating.HTMLRequest(self.client)
956         filterspec = request.filterspec
957         sort = request.sort
958         group = request.group
959         columns = request.columns
960         klass = self.db.getclass(request.classname)
962         # full-text search
963         if request.search_text:
964             matches = self.db.indexer.search(
965                 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
966         else:
967             matches = None
969         h = self.client.additional_headers
970         h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
971         # some browsers will honor the filename here...
972         h['Content-Disposition'] = 'inline; filename=query.csv'
974         self.client.header()
976         if self.client.env['REQUEST_METHOD'] == 'HEAD':
977             # all done, return a dummy string
978             return 'dummy'
980         wfile = self.client.request.wfile
981         if self.client.charset != self.client.STORAGE_CHARSET:
982             wfile = codecs.EncodedFile(wfile,
983                 self.client.STORAGE_CHARSET, self.client.charset, 'replace')
985         writer = csv.writer(wfile)
986         self.client._socket_op(writer.writerow, columns)
988         # and search
989         for itemid in klass.filter(matches, filterspec, sort, group):
990             self.client._socket_op(writer.writerow, [str(klass.get(itemid, col)) for col in columns])
992         return '\n'
994 # vim: set filetype=python sts=4 sw=4 et si :