92e62d42c31fd1664bbf5564c3457ed98ed58497
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 for propname, proptype in self.db.user.getprops().items():
868 value = user_props.get(propname, None)
869 if value is None:
870 pass
871 elif isinstance(proptype, hyperdb.Date):
872 user_props[propname] = str(value)
873 elif isinstance(proptype, hyperdb.Interval):
874 user_props[propname] = str(value)
875 elif isinstance(proptype, hyperdb.Password):
876 user_props[propname] = str(value)
877 otks = self.db.getOTKManager()
878 otk = ''.join([random.choice(chars) for x in range(32)])
879 while otks.exists(otk):
880 otk = ''.join([random.choice(chars) for x in range(32)])
881 otks.set(otk, **user_props)
883 # send the email
884 tracker_name = self.db.config.TRACKER_NAME
885 tracker_email = self.db.config.TRACKER_EMAIL
886 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
887 subject = 'Complete your registration to %s -- key %s'%(tracker_name,
888 otk)
889 body = """To complete your registration of the user "%(name)s" with
890 %(tracker)s, please do one of the following:
892 - send a reply to %(tracker_email)s and maintain the subject line as is (the
893 reply's additional "Re:" is ok),
895 - or visit the following URL:
897 %(url)s?@action=confrego&otk=%(otk)s
899 """ % {'name': user_props['username'], 'tracker': tracker_name,
900 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
901 else:
902 subject = 'Complete your registration to %s'%(tracker_name)
903 body = """To complete your registration of the user "%(name)s" with
904 %(tracker)s, please visit the following URL:
906 %(url)s?@action=confrego&otk=%(otk)s
908 """ % {'name': user_props['username'], 'tracker': tracker_name,
909 'url': self.base, 'otk': otk}
910 if not self.client.standard_message([user_props['address']], subject,
911 body, (tracker_name, tracker_email)):
912 return
914 # commit changes to the database
915 self.db.commit()
917 # redirect to the "you're almost there" page
918 raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
920 def newItemPermission(self, props, classname=None):
921 """Just check the "Register" permission.
922 """
923 # registration isn't allowed to supply roles
924 if props.has_key('roles'):
925 raise exceptions.Unauthorised, self._(
926 "It is not permitted to supply roles at registration.")
928 # technically already checked, but here for clarity
929 return self.hasPermission('Register', classname=classname)
931 class LogoutAction(Action):
932 def handle(self):
933 """Make us really anonymous - nuke the session too."""
934 # log us out
935 self.client.make_user_anonymous()
936 self.client.session_api.destroy()
938 # Let the user know what's going on
939 self.client.ok_message.append(self._('You are logged out'))
941 # reset client context to render tracker home page
942 # instead of last viewed page (may be inaccessibe for anonymous)
943 self.client.classname = None
944 self.client.nodeid = None
945 self.client.template = None
947 class LoginAction(Action):
948 def handle(self):
949 """Attempt to log a user in.
951 Sets up a session for the user which contains the login credentials.
953 """
954 # ensure modification comes via POST
955 if self.client.env['REQUEST_METHOD'] != 'POST':
956 raise roundup.exceptions.Reject(self._('Invalid request'))
958 # we need the username at a minimum
959 if not self.form.has_key('__login_name'):
960 self.client.error_message.append(self._('Username required'))
961 return
963 # get the login info
964 self.client.user = self.form['__login_name'].value
965 if self.form.has_key('__login_password'):
966 password = self.form['__login_password'].value
967 else:
968 password = ''
970 try:
971 self.verifyLogin(self.client.user, password)
972 except exceptions.LoginError, err:
973 self.client.make_user_anonymous()
974 self.client.error_message.extend(list(err.args))
975 return
977 # now we're OK, re-open the database for real, using the user
978 self.client.opendb(self.client.user)
980 # save user in session
981 self.client.session_api.set(user=self.client.user)
982 if self.form.has_key('remember'):
983 self.client.session_api.update(set_cookie=True, expire=24*3600*365)
985 # If we came from someplace, go back there
986 if self.form.has_key('__came_from'):
987 raise exceptions.Redirect, self.form['__came_from'].value
989 def verifyLogin(self, username, password):
990 # make sure the user exists
991 try:
992 self.client.userid = self.db.user.lookup(username)
993 except KeyError:
994 raise exceptions.LoginError, self._('Invalid login')
996 # verify the password
997 if not self.verifyPassword(self.client.userid, password):
998 raise exceptions.LoginError, self._('Invalid login')
1000 # Determine whether the user has permission to log in.
1001 # Base behaviour is to check the user has "Web Access".
1002 if not self.hasPermission("Web Access"):
1003 raise exceptions.LoginError, self._(
1004 "You do not have permission to login")
1006 def verifyPassword(self, userid, password):
1007 '''Verify the password that the user has supplied'''
1008 stored = self.db.user.get(userid, 'password')
1009 if password == stored:
1010 return 1
1011 if not password and not stored:
1012 return 1
1013 return 0
1015 class ExportCSVAction(Action):
1016 name = 'export'
1017 permissionType = 'View'
1019 def handle(self):
1020 ''' Export the specified search query as CSV. '''
1021 # figure the request
1022 request = templating.HTMLRequest(self.client)
1023 filterspec = request.filterspec
1024 sort = request.sort
1025 group = request.group
1026 columns = request.columns
1027 klass = self.db.getclass(request.classname)
1029 # full-text search
1030 if request.search_text:
1031 matches = self.db.indexer.search(
1032 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
1033 else:
1034 matches = None
1036 h = self.client.additional_headers
1037 h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
1038 # some browsers will honor the filename here...
1039 h['Content-Disposition'] = 'inline; filename=query.csv'
1041 self.client.header()
1043 if self.client.env['REQUEST_METHOD'] == 'HEAD':
1044 # all done, return a dummy string
1045 return 'dummy'
1047 wfile = self.client.request.wfile
1048 if self.client.charset != self.client.STORAGE_CHARSET:
1049 wfile = codecs.EncodedFile(wfile,
1050 self.client.STORAGE_CHARSET, self.client.charset, 'replace')
1052 writer = csv.writer(wfile)
1053 self.client._socket_op(writer.writerow, columns)
1055 # and search
1056 for itemid in klass.filter(matches, filterspec, sort, group):
1057 row = []
1058 for name in columns:
1059 # check permission to view this property on this item
1060 if not self.hasPermission('View', itemid=itemid,
1061 classname=request.classname, property=name):
1062 raise exceptions.Unauthorised, self._(
1063 'You do not have permission to view %(class)s'
1064 ) % {'class': request.classname}
1065 row.append(str(klass.get(itemid, name)))
1066 self.client._socket_op(writer.writerow, row)
1068 return '\n'
1071 class Bridge(BaseAction):
1072 """Make roundup.actions.Action executable via CGI request.
1074 Using this allows users to write actions executable from multiple frontends.
1075 CGI Form content is translated into a dictionary, which then is passed as
1076 argument to 'handle()'. XMLRPC requests have to pass this dictionary
1077 directly.
1078 """
1080 def __init__(self, *args):
1082 # As this constructor is callable from multiple frontends, each with
1083 # different Action interfaces, we have to look at the arguments to
1084 # figure out how to complete construction.
1085 if (len(args) == 1 and
1086 hasattr(args[0], '__class__') and
1087 args[0].__class__.__name__ == 'Client'):
1088 self.cgi = True
1089 self.execute = self.execute_cgi
1090 self.client = args[0]
1091 self.form = self.client.form
1092 else:
1093 self.cgi = False
1095 def execute_cgi(self):
1096 args = {}
1097 for key in self.form.keys():
1098 args[key] = self.form.getvalue(key)
1099 self.permission(args)
1100 return self.handle(args)
1102 def permission(self, args):
1103 """Raise Unauthorised if the current user is not allowed to execute
1104 this action. Users may override this method."""
1106 pass
1108 def handle(self, args):
1110 raise NotImplementedError
1112 # vim: set filetype=python sts=4 sw=4 et si :