eaf4c28a5b57080a455702ecefc997e1d94b3fad
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 else:
324 exists = 1
326 # confirm correct weight
327 if len(props_without_id) != len(values):
328 self.client.error_message.append(
329 self._('Not enough values on line %(line)s')%{'line':line})
330 return
332 # extract the new values
333 d = {}
334 for name, value in zip(props_without_id, values):
335 # check permission to edit this property on this item
336 if exists and not self.hasPermission('Edit', itemid=itemid,
337 classname=self.classname, property=name):
338 raise exceptions.Unauthorised, self._(
339 'You do not have permission to edit %(class)s'
340 ) % {'class': self.classname}
342 prop = cl.properties[name]
343 value = value.strip()
344 # only add the property if it has a value
345 if value:
346 # if it's a multilink, split it
347 if isinstance(prop, hyperdb.Multilink):
348 value = value.split(':')
349 elif isinstance(prop, hyperdb.Password):
350 value = password.Password(value)
351 elif isinstance(prop, hyperdb.Interval):
352 value = date.Interval(value)
353 elif isinstance(prop, hyperdb.Date):
354 value = date.Date(value)
355 elif isinstance(prop, hyperdb.Boolean):
356 value = value.lower() in ('yes', 'true', 'on', '1')
357 elif isinstance(prop, hyperdb.Number):
358 value = float(value)
359 d[name] = value
360 elif exists:
361 # nuke the existing value
362 if isinstance(prop, hyperdb.Multilink):
363 d[name] = []
364 else:
365 d[name] = None
367 # perform the edit
368 if exists:
369 # edit existing
370 cl.set(itemid, **d)
371 else:
372 # new node
373 found[cl.create(**d)] = 1
375 # retire the removed entries
376 for itemid in cl.list():
377 if not found.has_key(itemid):
378 # check permission to retire this item
379 if not self.hasPermission('Retire', itemid=itemid,
380 classname=self.classname):
381 raise exceptions.Unauthorised, self._(
382 'You do not have permission to retire %(class)s'
383 ) % {'class': self.classname}
384 cl.retire(itemid)
386 # all OK
387 self.db.commit()
389 self.client.ok_message.append(self._('Items edited OK'))
391 class EditCommon(Action):
392 '''Utility methods for editing.'''
394 def _editnodes(self, all_props, all_links):
395 ''' Use the props in all_props to perform edit and creation, then
396 use the link specs in all_links to do linking.
397 '''
398 # figure dependencies and re-work links
399 deps = {}
400 links = {}
401 for cn, nodeid, propname, vlist in all_links:
402 numeric_id = int (nodeid or 0)
403 if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
404 # link item to link to doesn't (and won't) exist
405 continue
407 for value in vlist:
408 if not all_props.has_key(value):
409 # link item to link to doesn't (and won't) exist
410 continue
411 deps.setdefault((cn, nodeid), []).append(value)
412 links.setdefault(value, []).append((cn, nodeid, propname))
414 # figure chained dependencies ordering
415 order = []
416 done = {}
417 # loop detection
418 change = 0
419 while len(all_props) != len(done):
420 for needed in all_props.keys():
421 if done.has_key(needed):
422 continue
423 tlist = deps.get(needed, [])
424 for target in tlist:
425 if not done.has_key(target):
426 break
427 else:
428 done[needed] = 1
429 order.append(needed)
430 change = 1
431 if not change:
432 raise ValueError, 'linking must not loop!'
434 # now, edit / create
435 m = []
436 for needed in order:
437 props = all_props[needed]
438 cn, nodeid = needed
439 if props:
440 if nodeid is not None and int(nodeid) > 0:
441 # make changes to the node
442 props = self._changenode(cn, nodeid, props)
444 # and some nice feedback for the user
445 if props:
446 info = ', '.join(map(self._, props.keys()))
447 m.append(
448 self._('%(class)s %(id)s %(properties)s edited ok')
449 % {'class':cn, 'id':nodeid, 'properties':info})
450 else:
451 m.append(self._('%(class)s %(id)s - nothing changed')
452 % {'class':cn, 'id':nodeid})
453 else:
454 assert props
456 # make a new node
457 newid = self._createnode(cn, props)
458 if nodeid is None:
459 self.nodeid = newid
460 nodeid = newid
462 # and some nice feedback for the user
463 m.append(self._('%(class)s %(id)s created')
464 % {'class':cn, 'id':newid})
466 # fill in new ids in links
467 if links.has_key(needed):
468 for linkcn, linkid, linkprop in links[needed]:
469 props = all_props[(linkcn, linkid)]
470 cl = self.db.classes[linkcn]
471 propdef = cl.getprops()[linkprop]
472 if not props.has_key(linkprop):
473 if linkid is None or linkid.startswith('-'):
474 # linking to a new item
475 if isinstance(propdef, hyperdb.Multilink):
476 props[linkprop] = [newid]
477 else:
478 props[linkprop] = newid
479 else:
480 # linking to an existing item
481 if isinstance(propdef, hyperdb.Multilink):
482 existing = cl.get(linkid, linkprop)[:]
483 existing.append(nodeid)
484 props[linkprop] = existing
485 else:
486 props[linkprop] = newid
488 return '<br>'.join(m)
490 def _changenode(self, cn, nodeid, props):
491 """Change the node based on the contents of the form."""
492 # check for permission
493 if not self.editItemPermission(props, classname=cn, itemid=nodeid):
494 raise exceptions.Unauthorised, self._(
495 'You do not have permission to edit %(class)s'
496 ) % {'class': cn}
498 # make the changes
499 cl = self.db.classes[cn]
500 return cl.set(nodeid, **props)
502 def _createnode(self, cn, props):
503 """Create a node based on the contents of the form."""
504 # check for permission
505 if not self.newItemPermission(props, classname=cn):
506 raise exceptions.Unauthorised, self._(
507 'You do not have permission to create %(class)s'
508 ) % {'class': cn}
510 # create the node and return its id
511 cl = self.db.classes[cn]
512 return cl.create(**props)
514 def isEditingSelf(self):
515 """Check whether a user is editing his/her own details."""
516 return (self.nodeid == self.userid
517 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
519 _cn_marker = []
520 def editItemPermission(self, props, classname=_cn_marker, itemid=None):
521 """Determine whether the user has permission to edit this item."""
522 if itemid is None:
523 itemid = self.nodeid
524 if classname is self._cn_marker:
525 classname = self.classname
526 # The user must have permission to edit each of the properties
527 # being changed.
528 for p in props:
529 if not self.hasPermission('Edit', itemid=itemid,
530 classname=classname, property=p):
531 return 0
532 # Since the user has permission to edit all of the properties,
533 # the edit is OK.
534 return 1
536 def newItemPermission(self, props, classname=None):
537 """Determine whether the user has permission to create this item.
539 Base behaviour is to check the user can edit this class. No additional
540 property checks are made.
541 """
543 if not classname :
544 classname = self.client.classname
546 if not self.hasPermission('Create', classname=classname):
547 return 0
549 # Check Edit permission for each property, to avoid being able
550 # to set restricted ones on new item creation
551 for key in props:
552 if not self.hasPermission('Edit', classname=classname,
553 property=key):
554 # We restrict by default and special-case allowed properties
555 if key == 'date' or key == 'content':
556 continue
557 elif key == 'author' and props[key] == self.userid:
558 continue
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 # registration isn't allowed to supply roles
844 user_props = props[('user', None)]
845 if user_props.has_key('roles'):
846 raise exceptions.Unauthorised, self._(
847 "It is not permitted to supply roles at registration.")
849 # skip the confirmation step?
850 if self.db.config['INSTANT_REGISTRATION']:
851 # handle the create now
852 try:
853 # when it hits the None element, it'll set self.nodeid
854 messages = self._editnodes(props, links)
855 except (ValueError, KeyError, IndexError,
856 roundup.exceptions.Reject), message:
857 # these errors might just be indicative of user dumbness
858 self.client.error_message.append(_('Error: %s') % str(message))
859 return
861 # fix up the initial roles
862 self.db.user.set(self.nodeid,
863 roles=self.db.config['NEW_WEB_USER_ROLES'])
865 # commit now that all the tricky stuff is done
866 self.db.commit()
868 # finish off by logging the user in
869 self.userid = self.nodeid
870 return self.finishRego()
872 # generate the one-time-key and store the props for later
873 for propname, proptype in self.db.user.getprops().items():
874 value = user_props.get(propname, None)
875 if value is None:
876 pass
877 elif isinstance(proptype, hyperdb.Date):
878 user_props[propname] = str(value)
879 elif isinstance(proptype, hyperdb.Interval):
880 user_props[propname] = str(value)
881 elif isinstance(proptype, hyperdb.Password):
882 user_props[propname] = str(value)
883 otks = self.db.getOTKManager()
884 otk = ''.join([random.choice(chars) for x in range(32)])
885 while otks.exists(otk):
886 otk = ''.join([random.choice(chars) for x in range(32)])
887 otks.set(otk, **user_props)
889 # send the email
890 tracker_name = self.db.config.TRACKER_NAME
891 tracker_email = self.db.config.TRACKER_EMAIL
892 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
893 subject = 'Complete your registration to %s -- key %s'%(tracker_name,
894 otk)
895 body = """To complete your registration of the user "%(name)s" with
896 %(tracker)s, please do one of the following:
898 - send a reply to %(tracker_email)s and maintain the subject line as is (the
899 reply's additional "Re:" is ok),
901 - or visit the following URL:
903 %(url)s?@action=confrego&otk=%(otk)s
905 """ % {'name': user_props['username'], 'tracker': tracker_name,
906 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
907 else:
908 subject = 'Complete your registration to %s'%(tracker_name)
909 body = """To complete your registration of the user "%(name)s" with
910 %(tracker)s, please visit the following URL:
912 %(url)s?@action=confrego&otk=%(otk)s
914 """ % {'name': user_props['username'], 'tracker': tracker_name,
915 'url': self.base, 'otk': otk}
916 if not self.client.standard_message([user_props['address']], subject,
917 body, (tracker_name, tracker_email)):
918 return
920 # commit changes to the database
921 self.db.commit()
923 # redirect to the "you're almost there" page
924 raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
926 class LogoutAction(Action):
927 def handle(self):
928 """Make us really anonymous - nuke the session too."""
929 # log us out
930 self.client.make_user_anonymous()
931 self.client.session_api.destroy()
933 # Let the user know what's going on
934 self.client.ok_message.append(self._('You are logged out'))
936 # reset client context to render tracker home page
937 # instead of last viewed page (may be inaccessibe for anonymous)
938 self.client.classname = None
939 self.client.nodeid = None
940 self.client.template = None
942 class LoginAction(Action):
943 def handle(self):
944 """Attempt to log a user in.
946 Sets up a session for the user which contains the login credentials.
948 """
949 # ensure modification comes via POST
950 if self.client.env['REQUEST_METHOD'] != 'POST':
951 raise roundup.exceptions.Reject(self._('Invalid request'))
953 # we need the username at a minimum
954 if not self.form.has_key('__login_name'):
955 self.client.error_message.append(self._('Username required'))
956 return
958 # get the login info
959 self.client.user = self.form['__login_name'].value
960 if self.form.has_key('__login_password'):
961 password = self.form['__login_password'].value
962 else:
963 password = ''
965 try:
966 self.verifyLogin(self.client.user, password)
967 except exceptions.LoginError, err:
968 self.client.make_user_anonymous()
969 self.client.error_message.extend(list(err.args))
970 return
972 # now we're OK, re-open the database for real, using the user
973 self.client.opendb(self.client.user)
975 # save user in session
976 self.client.session_api.set(user=self.client.user)
977 if self.form.has_key('remember'):
978 self.client.session_api.update(set_cookie=True, expire=24*3600*365)
980 # If we came from someplace, go back there
981 if self.form.has_key('__came_from'):
982 raise exceptions.Redirect, self.form['__came_from'].value
984 def verifyLogin(self, username, password):
985 # make sure the user exists
986 try:
987 self.client.userid = self.db.user.lookup(username)
988 except KeyError:
989 raise exceptions.LoginError, self._('Invalid login')
991 # verify the password
992 if not self.verifyPassword(self.client.userid, password):
993 raise exceptions.LoginError, self._('Invalid login')
995 # Determine whether the user has permission to log in.
996 # Base behaviour is to check the user has "Web Access".
997 if not self.hasPermission("Web Access"):
998 raise exceptions.LoginError, self._(
999 "You do not have permission to login")
1001 def verifyPassword(self, userid, password):
1002 '''Verify the password that the user has supplied'''
1003 stored = self.db.user.get(userid, 'password')
1004 if password == stored:
1005 return 1
1006 if not password and not stored:
1007 return 1
1008 return 0
1010 class ExportCSVAction(Action):
1011 name = 'export'
1012 permissionType = 'View'
1014 def handle(self):
1015 ''' Export the specified search query as CSV. '''
1016 # figure the request
1017 request = templating.HTMLRequest(self.client)
1018 filterspec = request.filterspec
1019 sort = request.sort
1020 group = request.group
1021 columns = request.columns
1022 klass = self.db.getclass(request.classname)
1024 # full-text search
1025 if request.search_text:
1026 matches = self.db.indexer.search(
1027 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
1028 else:
1029 matches = None
1031 h = self.client.additional_headers
1032 h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
1033 # some browsers will honor the filename here...
1034 h['Content-Disposition'] = 'inline; filename=query.csv'
1036 self.client.header()
1038 if self.client.env['REQUEST_METHOD'] == 'HEAD':
1039 # all done, return a dummy string
1040 return 'dummy'
1042 wfile = self.client.request.wfile
1043 if self.client.charset != self.client.STORAGE_CHARSET:
1044 wfile = codecs.EncodedFile(wfile,
1045 self.client.STORAGE_CHARSET, self.client.charset, 'replace')
1047 writer = csv.writer(wfile)
1048 self.client._socket_op(writer.writerow, columns)
1050 # and search
1051 for itemid in klass.filter(matches, filterspec, sort, group):
1052 row = []
1053 for name in columns:
1054 # check permission to view this property on this item
1055 if not self.hasPermission('View', itemid=itemid,
1056 classname=request.classname, property=name):
1057 raise exceptions.Unauthorised, self._(
1058 'You do not have permission to view %(class)s'
1059 ) % {'class': request.classname}
1060 row.append(str(klass.get(itemid, name)))
1061 self.client._socket_op(writer.writerow, row)
1063 return '\n'
1066 class Bridge(BaseAction):
1067 """Make roundup.actions.Action executable via CGI request.
1069 Using this allows users to write actions executable from multiple frontends.
1070 CGI Form content is translated into a dictionary, which then is passed as
1071 argument to 'handle()'. XMLRPC requests have to pass this dictionary
1072 directly.
1073 """
1075 def __init__(self, *args):
1077 # As this constructor is callable from multiple frontends, each with
1078 # different Action interfaces, we have to look at the arguments to
1079 # figure out how to complete construction.
1080 if (len(args) == 1 and
1081 hasattr(args[0], '__class__') and
1082 args[0].__class__.__name__ == 'Client'):
1083 self.cgi = True
1084 self.execute = self.execute_cgi
1085 self.client = args[0]
1086 self.form = self.client.form
1087 else:
1088 self.cgi = False
1090 def execute_cgi(self):
1091 args = {}
1092 for key in self.form.keys():
1093 args[key] = self.form.getvalue(key)
1094 self.permission(args)
1095 return self.handle(args)
1097 def permission(self, args):
1098 """Raise Unauthorised if the current user is not allowed to execute
1099 this action. Users may override this method."""
1101 pass
1103 def handle(self, args):
1105 raise NotImplementedError
1107 # vim: set filetype=python sts=4 sw=4 et si :