dd86a84da6f56fce0227f9855bc6010912d20675
1 import re, cgi, StringIO, urllib, time, random, csv, codecs
3 from roundup import hyperdb, token, date, password
4 from roundup.actions import Action as BaseAction
5 from roundup.i18n import _
6 import roundup.exceptions
7 from roundup.cgi import exceptions, templating
8 from roundup.mailgw import uidFromAddress
10 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
11 'EditCSVAction', 'EditItemAction', 'PassResetAction',
12 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
13 'NewItemAction', 'ExportCSVAction']
15 # used by a couple of routines
16 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
18 class Action:
19 def __init__(self, client):
20 self.client = client
21 self.form = client.form
22 self.db = client.db
23 self.nodeid = client.nodeid
24 self.template = client.template
25 self.classname = client.classname
26 self.userid = client.userid
27 self.base = client.base
28 self.user = client.user
29 self.context = templating.context(client)
31 def handle(self):
32 """Action handler procedure"""
33 raise NotImplementedError
35 def execute(self):
36 """Execute the action specified by this object."""
37 self.permission()
38 return self.handle()
40 name = ''
41 permissionType = None
42 def permission(self):
43 """Check whether the user has permission to execute this action.
45 True by default. If the permissionType attribute is a string containing
46 a simple permission, check whether the user has that permission.
47 Subclasses must also define the name attribute if they define
48 permissionType.
50 Despite having this permission, users may still be unauthorised to
51 perform parts of actions. It is up to the subclasses to detect this.
52 """
53 if (self.permissionType and
54 not self.hasPermission(self.permissionType)):
55 info = {'action': self.name, 'classname': self.classname}
56 raise exceptions.Unauthorised, self._(
57 'You do not have permission to '
58 '%(action)s the %(classname)s class.')%info
60 _marker = []
61 def hasPermission(self, permission, classname=_marker, itemid=None, property=None):
62 """Check whether the user has 'permission' on the current class."""
63 if classname is self._marker:
64 classname = self.client.classname
65 return self.db.security.hasPermission(permission, self.client.userid,
66 classname=classname, itemid=itemid, property=property)
68 def gettext(self, msgid):
69 """Return the localized translation of msgid"""
70 return self.client.translator.gettext(msgid)
72 _ = gettext
74 class ShowAction(Action):
76 typere=re.compile('[@:]type')
77 numre=re.compile('[@:]number')
79 def handle(self):
80 """Show a node of a particular class/id."""
81 t = n = ''
82 for key in self.form.keys():
83 if self.typere.match(key):
84 t = self.form[key].value.strip()
85 elif self.numre.match(key):
86 n = self.form[key].value.strip()
87 if not t:
88 raise ValueError, self._('No type specified')
89 if not n:
90 raise exceptions.SeriousError, self._('No ID entered')
91 try:
92 int(n)
93 except ValueError:
94 d = {'input': n, 'classname': t}
95 raise exceptions.SeriousError, self._(
96 '"%(input)s" is not an ID (%(classname)s ID required)')%d
97 url = '%s%s%s'%(self.base, t, n)
98 raise exceptions.Redirect, url
100 class RetireAction(Action):
101 name = 'retire'
102 permissionType = 'Edit'
104 def handle(self):
105 """Retire the context item."""
106 # ensure modification comes via POST
107 if self.client.env['REQUEST_METHOD'] != 'POST':
108 raise roundup.exceptions.Reject(self._('Invalid request'))
110 # if we want to view the index template now, then unset the itemid
111 # context info (a special-case for retire actions on the index page)
112 itemid = self.nodeid
113 if self.template == 'index':
114 self.client.nodeid = None
116 # make sure we don't try to retire admin or anonymous
117 if self.classname == 'user' and \
118 self.db.user.get(itemid, 'username') in ('admin', 'anonymous'):
119 raise ValueError, self._(
120 'You may not retire the admin or anonymous user')
122 # check permission
123 if not self.hasPermission('Retire', classname=self.classname,
124 itemid=itemid):
125 raise exceptions.Unauthorised, self._(
126 'You do not have permission to retire %(class)s'
127 ) % {'class': self.classname}
129 # do the retire
130 self.db.getclass(self.classname).retire(itemid)
131 self.db.commit()
133 self.client.ok_message.append(
134 self._('%(classname)s %(itemid)s has been retired')%{
135 'classname': self.classname.capitalize(), 'itemid': itemid})
138 class SearchAction(Action):
139 name = 'search'
140 permissionType = 'View'
142 def handle(self):
143 """Mangle some of the form variables.
145 Set the form ":filter" variable based on the values of the filter
146 variables - if they're set to anything other than "dontcare" then add
147 them to :filter.
149 Handle the ":queryname" variable and save off the query to the user's
150 query list.
152 Split any String query values on whitespace and comma.
154 """
155 self.fakeFilterVars()
156 queryname = self.getQueryName()
158 # editing existing query name?
159 old_queryname = self.getFromForm('old-queryname')
161 # handle saving the query params
162 if queryname:
163 # parse the environment and figure what the query _is_
164 req = templating.HTMLRequest(self.client)
166 url = self.getCurrentURL(req)
168 key = self.db.query.getkey()
169 if key:
170 # edit the old way, only one query per name
171 try:
172 qid = self.db.query.lookup(old_queryname)
173 if not self.hasPermission('Edit', 'query', itemid=qid):
174 raise exceptions.Unauthorised, self._(
175 "You do not have permission to edit queries")
176 self.db.query.set(qid, klass=self.classname, url=url)
177 except KeyError:
178 # create a query
179 if not self.hasPermission('Create', 'query'):
180 raise exceptions.Unauthorised, self._(
181 "You do not have permission to store queries")
182 qid = self.db.query.create(name=queryname,
183 klass=self.classname, url=url)
184 else:
185 # edit the new way, query name not a key any more
186 # see if we match an existing private query
187 uid = self.db.getuid()
188 qids = self.db.query.filter(None, {'name': old_queryname,
189 'private_for': uid})
190 if not qids:
191 # ok, so there's not a private query for the current user
192 # - see if there's one created by them
193 qids = self.db.query.filter(None, {'name': old_queryname,
194 'creator': uid})
196 if qids and old_queryname:
197 # edit query - make sure we get an exact match on the name
198 for qid in qids:
199 if old_queryname != self.db.query.get(qid, 'name'):
200 continue
201 if not self.hasPermission('Edit', 'query', itemid=qid):
202 raise exceptions.Unauthorised, self._(
203 "You do not have permission to edit queries")
204 self.db.query.set(qid, klass=self.classname,
205 url=url, name=queryname)
206 else:
207 # create a query
208 if not self.hasPermission('Create', 'query'):
209 raise exceptions.Unauthorised, self._(
210 "You do not have permission to store queries")
211 qid = self.db.query.create(name=queryname,
212 klass=self.classname, url=url, private_for=uid)
214 # and add it to the user's query multilink
215 queries = self.db.user.get(self.userid, 'queries')
216 if qid not in queries:
217 queries.append(qid)
218 self.db.user.set(self.userid, queries=queries)
220 # commit the query change to the database
221 self.db.commit()
223 def fakeFilterVars(self):
224 """Add a faked :filter form variable for each filtering prop."""
225 cls = self.db.classes[self.classname]
226 for key in self.form.keys():
227 prop = cls.get_transitive_prop(key)
228 if not prop:
229 continue
230 if isinstance(self.form[key], type([])):
231 # search for at least one entry which is not empty
232 for minifield in self.form[key]:
233 if minifield.value:
234 break
235 else:
236 continue
237 else:
238 if not self.form[key].value:
239 continue
240 if isinstance(prop, hyperdb.String):
241 v = self.form[key].value
242 l = token.token_split(v)
243 if len(l) != 1 or l[0] != v:
244 self.form.value.remove(self.form[key])
245 # replace the single value with the split list
246 for v in l:
247 self.form.value.append(cgi.MiniFieldStorage(key, v))
249 self.form.value.append(cgi.MiniFieldStorage('@filter', key))
251 def getCurrentURL(self, req):
252 """Get current URL for storing as a query.
254 Note: We are removing the first character from the current URL,
255 because the leading '?' is not part of the query string.
257 Implementation note:
258 But maybe the template should be part of the stored query:
259 template = self.getFromForm('template')
260 if template:
261 return req.indexargs_url('', {'@template' : template})[1:]
262 """
263 return req.indexargs_url('', {})[1:]
265 def getFromForm(self, name):
266 for key in ('@' + name, ':' + name):
267 if self.form.has_key(key):
268 return self.form[key].value.strip()
269 return ''
271 def getQueryName(self):
272 return self.getFromForm('queryname')
274 class EditCSVAction(Action):
275 name = 'edit'
276 permissionType = 'Edit'
278 def handle(self):
279 """Performs an edit of all of a class' items in one go.
281 The "rows" CGI var defines the CSV-formatted entries for the class. New
282 nodes are identified by the ID 'X' (or any other non-existent ID) and
283 removed lines are retired.
284 """
285 # ensure modification comes via POST
286 if self.client.env['REQUEST_METHOD'] != 'POST':
287 raise roundup.exceptions.Reject(self._('Invalid request'))
289 # figure the properties list for the class
290 cl = self.db.classes[self.classname]
291 props_without_id = cl.getprops(protected=0).keys()
293 # the incoming CSV data will always have the properties in colums
294 # sorted and starting with the "id" column
295 props_without_id.sort()
296 props = ['id'] + props_without_id
298 # do the edit
299 rows = StringIO.StringIO(self.form['rows'].value)
300 reader = csv.reader(rows)
301 found = {}
302 line = 0
303 for values in reader:
304 line += 1
305 if line == 1: continue
306 # skip property names header
307 if values == props:
308 continue
310 # extract the itemid
311 itemid, values = values[0], values[1:]
312 found[itemid] = 1
314 # see if the node exists
315 if itemid in ('x', 'X') or not cl.hasnode(itemid):
316 exists = 0
318 # check permission to create this item
319 if not self.hasPermission('Create', classname=self.classname):
320 raise exceptions.Unauthorised, self._(
321 'You do not have permission to create %(class)s'
322 ) % {'class': self.classname}
323 elif cl.hasnode(itemid) and cl.is_retired(itemid):
324 # If a CSV line just mentions an id and the corresponding
325 # item is retired, then the item is restored.
326 cl.restore(itemid)
327 continue
328 else:
329 exists = 1
331 # confirm correct weight
332 if len(props_without_id) != len(values):
333 self.client.error_message.append(
334 self._('Not enough values on line %(line)s')%{'line':line})
335 return
337 # extract the new values
338 d = {}
339 for name, value in zip(props_without_id, values):
340 # check permission to edit this property on this item
341 if exists and not self.hasPermission('Edit', itemid=itemid,
342 classname=self.classname, property=name):
343 raise exceptions.Unauthorised, self._(
344 'You do not have permission to edit %(class)s'
345 ) % {'class': self.classname}
347 prop = cl.properties[name]
348 value = value.strip()
349 # only add the property if it has a value
350 if value:
351 # if it's a multilink, split it
352 if isinstance(prop, hyperdb.Multilink):
353 value = value.split(':')
354 elif isinstance(prop, hyperdb.Password):
355 value = password.Password(value)
356 elif isinstance(prop, hyperdb.Interval):
357 value = date.Interval(value)
358 elif isinstance(prop, hyperdb.Date):
359 value = date.Date(value)
360 elif isinstance(prop, hyperdb.Boolean):
361 value = value.lower() in ('yes', 'true', 'on', '1')
362 elif isinstance(prop, hyperdb.Number):
363 value = float(value)
364 d[name] = value
365 elif exists:
366 # nuke the existing value
367 if isinstance(prop, hyperdb.Multilink):
368 d[name] = []
369 else:
370 d[name] = None
372 # perform the edit
373 if exists:
374 # edit existing
375 cl.set(itemid, **d)
376 else:
377 # new node
378 found[cl.create(**d)] = 1
380 # retire the removed entries
381 for itemid in cl.list():
382 if not found.has_key(itemid):
383 # check permission to retire this item
384 if not self.hasPermission('Retire', itemid=itemid,
385 classname=self.classname):
386 raise exceptions.Unauthorised, self._(
387 'You do not have permission to retire %(class)s'
388 ) % {'class': self.classname}
389 cl.retire(itemid)
391 # all OK
392 self.db.commit()
394 self.client.ok_message.append(self._('Items edited OK'))
396 class EditCommon(Action):
397 '''Utility methods for editing.'''
399 def _editnodes(self, all_props, all_links):
400 ''' Use the props in all_props to perform edit and creation, then
401 use the link specs in all_links to do linking.
402 '''
403 # figure dependencies and re-work links
404 deps = {}
405 links = {}
406 for cn, nodeid, propname, vlist in all_links:
407 numeric_id = int (nodeid or 0)
408 if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
409 # link item to link to doesn't (and won't) exist
410 continue
412 for value in vlist:
413 if not all_props.has_key(value):
414 # link item to link to doesn't (and won't) exist
415 continue
416 deps.setdefault((cn, nodeid), []).append(value)
417 links.setdefault(value, []).append((cn, nodeid, propname))
419 # figure chained dependencies ordering
420 order = []
421 done = {}
422 # loop detection
423 change = 0
424 while len(all_props) != len(done):
425 for needed in all_props.keys():
426 if done.has_key(needed):
427 continue
428 tlist = deps.get(needed, [])
429 for target in tlist:
430 if not done.has_key(target):
431 break
432 else:
433 done[needed] = 1
434 order.append(needed)
435 change = 1
436 if not change:
437 raise ValueError, 'linking must not loop!'
439 # now, edit / create
440 m = []
441 for needed in order:
442 props = all_props[needed]
443 cn, nodeid = needed
444 if props:
445 if nodeid is not None and int(nodeid) > 0:
446 # make changes to the node
447 props = self._changenode(cn, nodeid, props)
449 # and some nice feedback for the user
450 if props:
451 info = ', '.join(map(self._, props.keys()))
452 m.append(
453 self._('%(class)s %(id)s %(properties)s edited ok')
454 % {'class':cn, 'id':nodeid, 'properties':info})
455 else:
456 m.append(self._('%(class)s %(id)s - nothing changed')
457 % {'class':cn, 'id':nodeid})
458 else:
459 assert props
461 # make a new node
462 newid = self._createnode(cn, props)
463 if nodeid is None:
464 self.nodeid = newid
465 nodeid = newid
467 # and some nice feedback for the user
468 m.append(self._('%(class)s %(id)s created')
469 % {'class':cn, 'id':newid})
471 # fill in new ids in links
472 if links.has_key(needed):
473 for linkcn, linkid, linkprop in links[needed]:
474 props = all_props[(linkcn, linkid)]
475 cl = self.db.classes[linkcn]
476 propdef = cl.getprops()[linkprop]
477 if not props.has_key(linkprop):
478 if linkid is None or linkid.startswith('-'):
479 # linking to a new item
480 if isinstance(propdef, hyperdb.Multilink):
481 props[linkprop] = [nodeid]
482 else:
483 props[linkprop] = nodeid
484 else:
485 # linking to an existing item
486 if isinstance(propdef, hyperdb.Multilink):
487 existing = cl.get(linkid, linkprop)[:]
488 existing.append(nodeid)
489 props[linkprop] = existing
490 else:
491 props[linkprop] = nodeid
493 return '<br>'.join(m)
495 def _changenode(self, cn, nodeid, props):
496 """Change the node based on the contents of the form."""
497 # check for permission
498 if not self.editItemPermission(props, classname=cn, itemid=nodeid):
499 raise exceptions.Unauthorised, self._(
500 'You do not have permission to edit %(class)s'
501 ) % {'class': cn}
503 # make the changes
504 cl = self.db.classes[cn]
505 return cl.set(nodeid, **props)
507 def _createnode(self, cn, props):
508 """Create a node based on the contents of the form."""
509 # check for permission
510 if not self.newItemPermission(props, classname=cn):
511 raise exceptions.Unauthorised, self._(
512 'You do not have permission to create %(class)s'
513 ) % {'class': cn}
515 # create the node and return its id
516 cl = self.db.classes[cn]
517 return cl.create(**props)
519 def isEditingSelf(self):
520 """Check whether a user is editing his/her own details."""
521 return (self.nodeid == self.userid
522 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
524 _cn_marker = []
525 def editItemPermission(self, props, classname=_cn_marker, itemid=None):
526 """Determine whether the user has permission to edit this item."""
527 if itemid is None:
528 itemid = self.nodeid
529 if classname is self._cn_marker:
530 classname = self.classname
531 # The user must have permission to edit each of the properties
532 # being changed.
533 for p in props:
534 if not self.hasPermission('Edit', itemid=itemid,
535 classname=classname, property=p):
536 return 0
537 # Since the user has permission to edit all of the properties,
538 # the edit is OK.
539 return 1
541 def newItemPermission(self, props, classname=None):
542 """Determine whether the user has permission to create this item.
544 Base behaviour is to check the user can edit this class. No additional
545 property checks are made.
546 """
548 if not classname :
549 classname = self.client.classname
551 if not self.hasPermission('Create', classname=classname):
552 return 0
554 # Check Create permission for each property, to avoid being able
555 # to set restricted ones on new item creation
556 for key in props:
557 if not self.hasPermission('Create', classname=classname,
558 property=key):
559 return 0
560 return 1
562 class EditItemAction(EditCommon):
563 def lastUserActivity(self):
564 if self.form.has_key(':lastactivity'):
565 d = date.Date(self.form[':lastactivity'].value)
566 elif self.form.has_key('@lastactivity'):
567 d = date.Date(self.form['@lastactivity'].value)
568 else:
569 return None
570 d.second = int(d.second)
571 return d
573 def lastNodeActivity(self):
574 cl = getattr(self.client.db, self.classname)
575 activity = cl.get(self.nodeid, 'activity').local(0)
576 activity.second = int(activity.second)
577 return activity
579 def detectCollision(self, user_activity, node_activity):
580 '''Check for a collision and return the list of props we edited
581 that conflict.'''
582 if user_activity and user_activity < node_activity:
583 props, links = self.client.parsePropsFromForm()
584 key = (self.classname, self.nodeid)
585 # we really only collide for direct prop edit conflicts
586 return props[key].keys()
587 else:
588 return []
590 def handleCollision(self, props):
591 message = self._('Edit Error: someone else has edited this %s (%s). '
592 'View <a target="new" href="%s%s">their changes</a> '
593 'in a new window.')%(self.classname, ', '.join(props),
594 self.classname, self.nodeid)
595 self.client.error_message.append(message)
596 return
598 def handle(self):
599 """Perform an edit of an item in the database.
601 See parsePropsFromForm and _editnodes for special variables.
603 """
604 # ensure modification comes via POST
605 if self.client.env['REQUEST_METHOD'] != 'POST':
606 raise roundup.exceptions.Reject(self._('Invalid request'))
608 user_activity = self.lastUserActivity()
609 if user_activity:
610 props = self.detectCollision(user_activity, self.lastNodeActivity())
611 if props:
612 self.handleCollision(props)
613 return
615 props, links = self.client.parsePropsFromForm()
617 # handle the props
618 try:
619 message = self._editnodes(props, links)
620 except (ValueError, KeyError, IndexError,
621 roundup.exceptions.Reject), message:
622 self.client.error_message.append(
623 self._('Edit Error: %s') % str(message))
624 return
626 # commit now that all the tricky stuff is done
627 self.db.commit()
629 # redirect to the item's edit page
630 # redirect to finish off
631 url = self.base + self.classname
632 # note that this action might have been called by an index page, so
633 # we will want to include index-page args in this URL too
634 if self.nodeid is not None:
635 url += self.nodeid
636 url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
637 urllib.quote(self.template))
638 if self.nodeid is None:
639 req = templating.HTMLRequest(self.client)
640 url += '&' + req.indexargs_url('', {})[1:]
641 raise exceptions.Redirect, url
643 class NewItemAction(EditCommon):
644 def handle(self):
645 ''' Add a new item to the database.
647 This follows the same form as the EditItemAction, with the same
648 special form values.
649 '''
650 # ensure modification comes via POST
651 if self.client.env['REQUEST_METHOD'] != 'POST':
652 raise roundup.exceptions.Reject(self._('Invalid request'))
654 # parse the props from the form
655 try:
656 props, links = self.client.parsePropsFromForm(create=1)
657 except (ValueError, KeyError), message:
658 self.client.error_message.append(self._('Error: %s')
659 % str(message))
660 return
662 # handle the props - edit or create
663 try:
664 # when it hits the None element, it'll set self.nodeid
665 messages = self._editnodes(props, links)
666 except (ValueError, KeyError, IndexError,
667 roundup.exceptions.Reject), message:
668 # these errors might just be indicative of user dumbness
669 self.client.error_message.append(_('Error: %s') % str(message))
670 return
672 # commit now that all the tricky stuff is done
673 self.db.commit()
675 # redirect to the new item's page
676 raise exceptions.Redirect, '%s%s%s?@ok_message=%s&@template=%s' % (
677 self.base, self.classname, self.nodeid, urllib.quote(messages),
678 urllib.quote(self.template))
680 class PassResetAction(Action):
681 def handle(self):
682 """Handle password reset requests.
684 Presence of either "name" or "address" generates email. Presence of
685 "otk" performs the reset.
687 """
688 otks = self.db.getOTKManager()
689 if self.form.has_key('otk'):
690 # pull the rego information out of the otk database
691 otk = self.form['otk'].value
692 uid = otks.get(otk, 'uid', default=None)
693 if uid is None:
694 self.client.error_message.append(
695 self._("Invalid One Time Key!\n"
696 "(a Mozilla bug may cause this message "
697 "to show up erroneously, please check your email)"))
698 return
700 # re-open the database as "admin"
701 if self.user != 'admin':
702 self.client.opendb('admin')
703 self.db = self.client.db
704 otks = self.db.getOTKManager()
706 # change the password
707 newpw = password.generatePassword()
709 cl = self.db.user
710 # XXX we need to make the "default" page be able to display errors!
711 try:
712 # set the password
713 cl.set(uid, password=password.Password(newpw))
714 # clear the props from the otk database
715 otks.destroy(otk)
716 self.db.commit()
717 except (ValueError, KeyError), message:
718 self.client.error_message.append(str(message))
719 return
721 # user info
722 address = self.db.user.get(uid, 'address')
723 name = self.db.user.get(uid, 'username')
725 # send the email
726 tracker_name = self.db.config.TRACKER_NAME
727 subject = 'Password reset for %s'%tracker_name
728 body = '''
729 The password has been reset for username "%(name)s".
731 Your password is now: %(password)s
732 '''%{'name': name, 'password': newpw}
733 if not self.client.standard_message([address], subject, body):
734 return
736 self.client.ok_message.append(
737 self._('Password reset and email sent to %s') % address)
738 return
740 # no OTK, so now figure the user
741 if self.form.has_key('username'):
742 name = self.form['username'].value
743 try:
744 uid = self.db.user.lookup(name)
745 except KeyError:
746 self.client.error_message.append(self._('Unknown username'))
747 return
748 address = self.db.user.get(uid, 'address')
749 elif self.form.has_key('address'):
750 address = self.form['address'].value
751 uid = uidFromAddress(self.db, ('', address), create=0)
752 if not uid:
753 self.client.error_message.append(
754 self._('Unknown email address'))
755 return
756 name = self.db.user.get(uid, 'username')
757 else:
758 self.client.error_message.append(
759 self._('You need to specify a username or address'))
760 return
762 # generate the one-time-key and store the props for later
763 otk = ''.join([random.choice(chars) for x in range(32)])
764 while otks.exists(otk):
765 otk = ''.join([random.choice(chars) for x in range(32)])
766 otks.set(otk, uid=uid)
767 self.db.commit()
769 # send the email
770 tracker_name = self.db.config.TRACKER_NAME
771 subject = 'Confirm reset of password for %s'%tracker_name
772 body = '''
773 Someone, perhaps you, has requested that the password be changed for your
774 username, "%(name)s". If you wish to proceed with the change, please follow
775 the link below:
777 %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
779 You should then receive another email with the new password.
780 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
781 if not self.client.standard_message([address], subject, body):
782 return
784 self.client.ok_message.append(self._('Email sent to %s') % address)
786 class RegoCommon(Action):
787 def finishRego(self):
788 # log the new user in
789 self.client.userid = self.userid
790 user = self.client.user = self.db.user.get(self.userid, 'username')
791 # re-open the database for real, using the user
792 self.client.opendb(user)
794 # update session data
795 self.client.session_api.set(user=user)
797 # nice message
798 message = self._('You are now registered, welcome!')
799 url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
800 urllib.quote(message))
802 # redirect to the user's page (but not 302, as some email clients seem
803 # to want to reload the page, or something)
804 return '''<html><head><title>%s</title></head>
805 <body><p><a href="%s">%s</a></p>
806 <script type="text/javascript">
807 window.setTimeout('window.location = "%s"', 1000);
808 </script>'''%(message, url, message, url)
810 class ConfRegoAction(RegoCommon):
811 def handle(self):
812 """Grab the OTK, use it to load up the new user details."""
813 try:
814 # pull the rego information out of the otk database
815 self.userid = self.db.confirm_registration(self.form['otk'].value)
816 except (ValueError, KeyError), message:
817 self.client.error_message.append(str(message))
818 return
819 return self.finishRego()
821 class RegisterAction(RegoCommon, EditCommon):
822 name = 'register'
823 permissionType = 'Register'
825 def handle(self):
826 """Attempt to create a new user based on the contents of the form
827 and then remember it in session.
829 Return 1 on successful login.
830 """
831 # ensure modification comes via POST
832 if self.client.env['REQUEST_METHOD'] != 'POST':
833 raise roundup.exceptions.Reject(self._('Invalid request'))
835 # parse the props from the form
836 try:
837 props, links = self.client.parsePropsFromForm(create=1)
838 except (ValueError, KeyError), message:
839 self.client.error_message.append(self._('Error: %s')
840 % str(message))
841 return
843 # skip the confirmation step?
844 if self.db.config['INSTANT_REGISTRATION']:
845 # handle the create now
846 try:
847 # when it hits the None element, it'll set self.nodeid
848 messages = self._editnodes(props, links)
849 except (ValueError, KeyError, IndexError,
850 roundup.exceptions.Reject), message:
851 # these errors might just be indicative of user dumbness
852 self.client.error_message.append(_('Error: %s') % str(message))
853 return
855 # fix up the initial roles
856 self.db.user.set(self.nodeid,
857 roles=self.db.config['NEW_WEB_USER_ROLES'])
859 # commit now that all the tricky stuff is done
860 self.db.commit()
862 # finish off by logging the user in
863 self.userid = self.nodeid
864 return self.finishRego()
866 # generate the one-time-key and store the props for later
867 user_props = props[('user', None)]
868 for propname, proptype in self.db.user.getprops().items():
869 value = user_props.get(propname, None)
870 if value is None:
871 pass
872 elif isinstance(proptype, hyperdb.Date):
873 user_props[propname] = str(value)
874 elif isinstance(proptype, hyperdb.Interval):
875 user_props[propname] = str(value)
876 elif isinstance(proptype, hyperdb.Password):
877 user_props[propname] = str(value)
878 otks = self.db.getOTKManager()
879 otk = ''.join([random.choice(chars) for x in range(32)])
880 while otks.exists(otk):
881 otk = ''.join([random.choice(chars) for x in range(32)])
882 otks.set(otk, **user_props)
884 # send the email
885 tracker_name = self.db.config.TRACKER_NAME
886 tracker_email = self.db.config.TRACKER_EMAIL
887 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
888 subject = 'Complete your registration to %s -- key %s'%(tracker_name,
889 otk)
890 body = """To complete your registration of the user "%(name)s" with
891 %(tracker)s, please do one of the following:
893 - send a reply to %(tracker_email)s and maintain the subject line as is (the
894 reply's additional "Re:" is ok),
896 - or visit the following URL:
898 %(url)s?@action=confrego&otk=%(otk)s
900 """ % {'name': user_props['username'], 'tracker': tracker_name,
901 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
902 else:
903 subject = 'Complete your registration to %s'%(tracker_name)
904 body = """To complete your registration of the user "%(name)s" with
905 %(tracker)s, please visit the following URL:
907 %(url)s?@action=confrego&otk=%(otk)s
909 """ % {'name': user_props['username'], 'tracker': tracker_name,
910 'url': self.base, 'otk': otk}
911 if not self.client.standard_message([user_props['address']], subject,
912 body, (tracker_name, tracker_email)):
913 return
915 # commit changes to the database
916 self.db.commit()
918 # redirect to the "you're almost there" page
919 raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
921 def newItemPermission(self, props, classname=None):
922 """Just check the "Register" permission.
923 """
924 # registration isn't allowed to supply roles
925 if props.has_key('roles'):
926 raise exceptions.Unauthorised, self._(
927 "It is not permitted to supply roles at registration.")
929 # technically already checked, but here for clarity
930 return self.hasPermission('Register', classname=classname)
932 class LogoutAction(Action):
933 def handle(self):
934 """Make us really anonymous - nuke the session too."""
935 # log us out
936 self.client.make_user_anonymous()
937 self.client.session_api.destroy()
939 # Let the user know what's going on
940 self.client.ok_message.append(self._('You are logged out'))
942 # reset client context to render tracker home page
943 # instead of last viewed page (may be inaccessibe for anonymous)
944 self.client.classname = None
945 self.client.nodeid = None
946 self.client.template = None
948 class LoginAction(Action):
949 def handle(self):
950 """Attempt to log a user in.
952 Sets up a session for the user which contains the login credentials.
954 """
955 # ensure modification comes via POST
956 if self.client.env['REQUEST_METHOD'] != 'POST':
957 raise roundup.exceptions.Reject(self._('Invalid request'))
959 # we need the username at a minimum
960 if not self.form.has_key('__login_name'):
961 self.client.error_message.append(self._('Username required'))
962 return
964 # get the login info
965 self.client.user = self.form['__login_name'].value
966 if self.form.has_key('__login_password'):
967 password = self.form['__login_password'].value
968 else:
969 password = ''
971 try:
972 self.verifyLogin(self.client.user, password)
973 except exceptions.LoginError, err:
974 self.client.make_user_anonymous()
975 self.client.error_message.extend(list(err.args))
976 return
978 # now we're OK, re-open the database for real, using the user
979 self.client.opendb(self.client.user)
981 # save user in session
982 self.client.session_api.set(user=self.client.user)
983 if self.form.has_key('remember'):
984 self.client.session_api.update(set_cookie=True, expire=24*3600*365)
986 # If we came from someplace, go back there
987 if self.form.has_key('__came_from'):
988 raise exceptions.Redirect, self.form['__came_from'].value
990 def verifyLogin(self, username, password):
991 # make sure the user exists
992 try:
993 self.client.userid = self.db.user.lookup(username)
994 except KeyError:
995 raise exceptions.LoginError, self._('Invalid login')
997 # verify the password
998 if not self.verifyPassword(self.client.userid, password):
999 raise exceptions.LoginError, self._('Invalid login')
1001 # Determine whether the user has permission to log in.
1002 # Base behaviour is to check the user has "Web Access".
1003 if not self.hasPermission("Web Access"):
1004 raise exceptions.LoginError, self._(
1005 "You do not have permission to login")
1007 def verifyPassword(self, userid, password):
1008 '''Verify the password that the user has supplied'''
1009 stored = self.db.user.get(userid, 'password')
1010 if password == stored:
1011 return 1
1012 if not password and not stored:
1013 return 1
1014 return 0
1016 class ExportCSVAction(Action):
1017 name = 'export'
1018 permissionType = 'View'
1020 def handle(self):
1021 ''' Export the specified search query as CSV. '''
1022 # figure the request
1023 request = templating.HTMLRequest(self.client)
1024 filterspec = request.filterspec
1025 sort = request.sort
1026 group = request.group
1027 columns = request.columns
1028 klass = self.db.getclass(request.classname)
1030 # full-text search
1031 if request.search_text:
1032 matches = self.db.indexer.search(
1033 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
1034 else:
1035 matches = None
1037 h = self.client.additional_headers
1038 h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
1039 # some browsers will honor the filename here...
1040 h['Content-Disposition'] = 'inline; filename=query.csv'
1042 self.client.header()
1044 if self.client.env['REQUEST_METHOD'] == 'HEAD':
1045 # all done, return a dummy string
1046 return 'dummy'
1048 wfile = self.client.request.wfile
1049 if self.client.charset != self.client.STORAGE_CHARSET:
1050 wfile = codecs.EncodedFile(wfile,
1051 self.client.STORAGE_CHARSET, self.client.charset, 'replace')
1053 writer = csv.writer(wfile)
1054 self.client._socket_op(writer.writerow, columns)
1056 # and search
1057 for itemid in klass.filter(matches, filterspec, sort, group):
1058 row = []
1059 for name in columns:
1060 # check permission to view this property on this item
1061 if not self.hasPermission('View', itemid=itemid,
1062 classname=request.classname, property=name):
1063 raise exceptions.Unauthorised, self._(
1064 'You do not have permission to view %(class)s'
1065 ) % {'class': request.classname}
1066 row.append(str(klass.get(itemid, name)))
1067 self.client._socket_op(writer.writerow, row)
1069 return '\n'
1072 class Bridge(BaseAction):
1073 """Make roundup.actions.Action executable via CGI request.
1075 Using this allows users to write actions executable from multiple frontends.
1076 CGI Form content is translated into a dictionary, which then is passed as
1077 argument to 'handle()'. XMLRPC requests have to pass this dictionary
1078 directly.
1079 """
1081 def __init__(self, *args):
1083 # As this constructor is callable from multiple frontends, each with
1084 # different Action interfaces, we have to look at the arguments to
1085 # figure out how to complete construction.
1086 if (len(args) == 1 and
1087 hasattr(args[0], '__class__') and
1088 args[0].__class__.__name__ == 'Client'):
1089 self.cgi = True
1090 self.execute = self.execute_cgi
1091 self.client = args[0]
1092 self.form = self.client.form
1093 else:
1094 self.cgi = False
1096 def execute_cgi(self):
1097 args = {}
1098 for key in self.form.keys():
1099 args[key] = self.form.getvalue(key)
1100 self.permission(args)
1101 return self.handle(args)
1103 def permission(self, args):
1104 """Raise Unauthorised if the current user is not allowed to execute
1105 this action. Users may override this method."""
1107 pass
1109 def handle(self, args):
1111 raise NotImplementedError
1113 # vim: set filetype=python sts=4 sw=4 et si :