diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index 529bfaca2d97ba2e9b57c2945bb16fd57edd8efa..86bd4f1f112a5e2e82eb936ad6d0a185da6033bd 100755 (executable)
--- a/roundup/cgi/actions.py
+++ b/roundup/cgi/actions.py
-#$Id: actions.py,v 1.16 2004-03-26 00:46:33 richard Exp $
+import re, cgi, time, random, csv, codecs
-import re, cgi, StringIO, urllib, Cookie, time, random
-
-from roundup import hyperdb, token, date, password, rcsv, exceptions
+from roundup import hyperdb, token, date, password
+from roundup.actions import Action as BaseAction
from roundup.i18n import _
-from roundup.cgi import templating
-from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
+import roundup.exceptions
+from roundup.cgi import exceptions, templating
from roundup.mailgw import uidFromAddress
+from roundup.anypy import io_, urllib_
__all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
'EditCSVAction', 'EditItemAction', 'PassResetAction',
self.userid = client.userid
self.base = client.base
self.user = client.user
+ self.context = templating.context(client)
+
+ def handle(self):
+ """Action handler procedure"""
+ raise NotImplementedError
def execute(self):
"""Execute the action specified by this object."""
self.permission()
- self.handle()
+ return self.handle()
name = ''
permissionType = None
if (self.permissionType and
not self.hasPermission(self.permissionType)):
info = {'action': self.name, 'classname': self.classname}
- raise Unauthorised, _('You do not have permission to '
- '%(action)s the %(classname)s class.')%info
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to '
+ '%(action)s the %(classname)s class.')%info)
- def hasPermission(self, permission):
+ _marker = []
+ def hasPermission(self, permission, classname=_marker, itemid=None, property=None):
"""Check whether the user has 'permission' on the current class."""
+ if classname is self._marker:
+ classname = self.client.classname
return self.db.security.hasPermission(permission, self.client.userid,
- self.client.classname)
+ classname=classname, itemid=itemid, property=property)
+
+ def gettext(self, msgid):
+ """Return the localized translation of msgid"""
+ return self.client.translator.gettext(msgid)
+
+ _ = gettext
class ShowAction(Action):
- def handle(self, typere=re.compile('[@:]type'),
- numre=re.compile('[@:]number')):
+
+ typere=re.compile('[@:]type')
+ numre=re.compile('[@:]number')
+
+ def handle(self):
"""Show a node of a particular class/id."""
t = n = ''
- for key in self.form.keys():
- if typere.match(key):
+ for key in self.form:
+ if self.typere.match(key):
t = self.form[key].value.strip()
- elif numre.match(key):
+ elif self.numre.match(key):
n = self.form[key].value.strip()
if not t:
- raise ValueError, 'No type specified'
+ raise ValueError(self._('No type specified'))
if not n:
- raise SeriousError, _('No ID entered')
+ raise exceptions.SeriousError(self._('No ID entered'))
try:
int(n)
except ValueError:
d = {'input': n, 'classname': t}
- raise SeriousError, _(
- '"%(input)s" is not an ID (%(classname)s ID required)')%d
- url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
- raise Redirect, url
+ raise exceptions.SeriousError(self._(
+ '"%(input)s" is not an ID (%(classname)s ID required)')%d)
+ url = '%s%s%s'%(self.base, t, n)
+ raise exceptions.Redirect(url)
class RetireAction(Action):
name = 'retire'
def handle(self):
"""Retire the context item."""
- # if we want to view the index template now, then unset the nodeid
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
+ # if we want to view the index template now, then unset the itemid
# context info (a special-case for retire actions on the index page)
- nodeid = self.nodeid
+ itemid = self.nodeid
if self.template == 'index':
self.client.nodeid = None
# make sure we don't try to retire admin or anonymous
if self.classname == 'user' and \
- self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
- raise ValueError, _('You may not retire the admin or anonymous user')
+ self.db.user.get(itemid, 'username') in ('admin', 'anonymous'):
+ raise ValueError(self._(
+ 'You may not retire the admin or anonymous user'))
+
+ # check permission
+ if not self.hasPermission('Retire', classname=self.classname,
+ itemid=itemid):
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to retire %(class)s'
+ ) % {'class': self.classname})
# do the retire
- self.db.getclass(self.classname).retire(nodeid)
+ self.db.getclass(self.classname).retire(itemid)
self.db.commit()
self.client.ok_message.append(
- _('%(classname)s %(itemid)s has been retired')%{
- 'classname': self.classname.capitalize(), 'itemid': nodeid})
+ self._('%(classname)s %(itemid)s has been retired')%{
+ 'classname': self.classname.capitalize(), 'itemid': itemid})
+
class SearchAction(Action):
name = 'search'
permissionType = 'View'
- def handle(self, wcre=re.compile(r'[\s,]+')):
+ def handle(self):
"""Mangle some of the form variables.
Set the form ":filter" variable based on the values of the filter
self.fakeFilterVars()
queryname = self.getQueryName()
+ # editing existing query name?
+ old_queryname = self.getFromForm('old-queryname')
+
# handle saving the query params
if queryname:
# parse the environment and figure what the query _is_
req = templating.HTMLRequest(self.client)
- # The [1:] strips off the '?' character, it isn't part of the
- # query string.
- url = req.indexargs_href('', {})[1:]
-
- # handle editing an existing query
- try:
- qid = self.db.query.lookup(queryname)
- self.db.query.set(qid, klass=self.classname, url=url)
- except KeyError:
- # create a query
- qid = self.db.query.create(name=queryname,
- klass=self.classname, url=url)
+ url = self.getCurrentURL(req)
+
+ key = self.db.query.getkey()
+ if key:
+ # edit the old way, only one query per name
+ try:
+ qid = self.db.query.lookup(old_queryname)
+ if not self.hasPermission('Edit', 'query', itemid=qid):
+ raise exceptions.Unauthorised(self._(
+ "You do not have permission to edit queries"))
+ self.db.query.set(qid, klass=self.classname, url=url)
+ except KeyError:
+ # create a query
+ if not self.hasPermission('Create', 'query'):
+ raise exceptions.Unauthorised(self._(
+ "You do not have permission to store queries"))
+ qid = self.db.query.create(name=queryname,
+ klass=self.classname, url=url)
+ else:
+ # edit the new way, query name not a key any more
+ # see if we match an existing private query
+ uid = self.db.getuid()
+ qids = self.db.query.filter(None, {'name': old_queryname,
+ 'private_for': uid})
+ if not qids:
+ # ok, so there's not a private query for the current user
+ # - see if there's one created by them
+ qids = self.db.query.filter(None, {'name': old_queryname,
+ 'creator': uid})
+
+ if qids and old_queryname:
+ # edit query - make sure we get an exact match on the name
+ for qid in qids:
+ if old_queryname != self.db.query.get(qid, 'name'):
+ continue
+ if not self.hasPermission('Edit', 'query', itemid=qid):
+ raise exceptions.Unauthorised(self._(
+ "You do not have permission to edit queries"))
+ self.db.query.set(qid, klass=self.classname,
+ url=url, name=queryname)
+ else:
+ # create a query
+ if not self.hasPermission('Create', 'query'):
+ raise exceptions.Unauthorised(self._(
+ "You do not have permission to store queries"))
+ qid = self.db.query.create(name=queryname,
+ klass=self.classname, url=url, private_for=uid)
# and add it to the user's query multilink
queries = self.db.user.get(self.userid, 'queries')
def fakeFilterVars(self):
"""Add a faked :filter form variable for each filtering prop."""
- props = self.db.classes[self.classname].getprops()
- for key in self.form.keys():
- if not props.has_key(key):
+ cls = self.db.classes[self.classname]
+ for key in self.form:
+ prop = cls.get_transitive_prop(key)
+ if not prop:
continue
if isinstance(self.form[key], type([])):
# search for at least one entry which is not empty
else:
if not self.form[key].value:
continue
- if isinstance(props[key], hyperdb.String):
+ if isinstance(prop, hyperdb.String):
v = self.form[key].value
l = token.token_split(v)
- if len(l) > 1 or l[0] != v:
+ if len(l) != 1 or l[0] != v:
self.form.value.remove(self.form[key])
# replace the single value with the split list
for v in l:
self.form.value.append(cgi.MiniFieldStorage('@filter', key))
- FV_QUERYNAME = re.compile(r'[@:]queryname')
- def getQueryName(self):
- for key in self.form.keys():
- if self.FV_QUERYNAME.match(key):
+ def getCurrentURL(self, req):
+ """Get current URL for storing as a query.
+
+ Note: We are removing the first character from the current URL,
+ because the leading '?' is not part of the query string.
+
+ Implementation note:
+ But maybe the template should be part of the stored query:
+ template = self.getFromForm('template')
+ if template:
+ return req.indexargs_url('', {'@template' : template})[1:]
+ """
+ return req.indexargs_url('', {})[1:]
+
+ def getFromForm(self, name):
+ for key in ('@' + name, ':' + name):
+ if key in self.form:
return self.form[key].value.strip()
return ''
+ def getQueryName(self):
+ return self.getFromForm('queryname')
+
class EditCSVAction(Action):
name = 'edit'
permissionType = 'Edit'
The "rows" CGI var defines the CSV-formatted entries for the class. New
nodes are identified by the ID 'X' (or any other non-existent ID) and
removed lines are retired.
-
"""
- # get the CSV module
- if rcsv.error:
- self.client.error_message.append(_(rcsv.error))
- return
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+ # figure the properties list for the class
cl = self.db.classes[self.classname]
- idlessprops = cl.getprops(protected=0).keys()
- idlessprops.sort()
- props = ['id'] + idlessprops
+ props_without_id = list(cl.getprops(protected=0))
+
+ # the incoming CSV data will always have the properties in colums
+ # sorted and starting with the "id" column
+ props_without_id.sort()
+ props = ['id'] + props_without_id
# do the edit
- rows = StringIO.StringIO(self.form['rows'].value)
- reader = rcsv.reader(rows, rcsv.comma_separated)
+ rows = io_.BytesIO(self.form['rows'].value)
+ reader = csv.reader(rows)
found = {}
line = 0
for values in reader:
if values == props:
continue
- # extract the nodeid
- nodeid, values = values[0], values[1:]
- found[nodeid] = 1
+ # extract the itemid
+ itemid, values = values[0], values[1:]
+ found[itemid] = 1
# see if the node exists
- if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
+ if itemid in ('x', 'X') or not cl.hasnode(itemid):
exists = 0
+
+ # check permission to create this item
+ if not self.hasPermission('Create', classname=self.classname):
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to create %(class)s'
+ ) % {'class': self.classname})
+ elif cl.hasnode(itemid) and cl.is_retired(itemid):
+ # If a CSV line just mentions an id and the corresponding
+ # item is retired, then the item is restored.
+ cl.restore(itemid)
+ continue
else:
exists = 1
# confirm correct weight
- if len(idlessprops) != len(values):
+ if len(props_without_id) != len(values):
self.client.error_message.append(
- _('Not enough values on line %(line)s')%{'line':line})
+ self._('Not enough values on line %(line)s')%{'line':line})
return
# extract the new values
d = {}
- for name, value in zip(idlessprops, values):
+ for name, value in zip(props_without_id, values):
+ # check permission to edit this property on this item
+ if exists and not self.hasPermission('Edit', itemid=itemid,
+ classname=self.classname, property=name):
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to edit %(class)s'
+ ) % {'class': self.classname})
+
prop = cl.properties[name]
value = value.strip()
# only add the property if it has a value
if isinstance(prop, hyperdb.Multilink):
value = value.split(':')
elif isinstance(prop, hyperdb.Password):
- value = password.Password(value)
+ value = password.Password(value, config=self.db.config)
elif isinstance(prop, hyperdb.Interval):
value = date.Interval(value)
elif isinstance(prop, hyperdb.Date):
# perform the edit
if exists:
# edit existing
- cl.set(nodeid, **d)
+ cl.set(itemid, **d)
else:
# new node
found[cl.create(**d)] = 1
# retire the removed entries
- for nodeid in cl.list():
- if not found.has_key(nodeid):
- cl.retire(nodeid)
+ for itemid in cl.list():
+ if itemid not in found:
+ # check permission to retire this item
+ if not self.hasPermission('Retire', itemid=itemid,
+ classname=self.classname):
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to retire %(class)s'
+ ) % {'class': self.classname})
+ cl.retire(itemid)
# all OK
self.db.commit()
- self.client.ok_message.append(_('Items edited OK'))
-
-class _EditAction(Action):
- def isEditingSelf(self):
- """Check whether a user is editing his/her own details."""
- return (self.nodeid == self.userid
- and self.db.user.get(self.nodeid, 'username') != 'anonymous')
-
- def editItemPermission(self, props):
- """Determine whether the user has permission to edit this item.
-
- Base behaviour is to check the user can edit this class. If we're
- editing the "user" class, users are allowed to edit their own details.
- Unless it's the "roles" property, which requires the special Permission
- "Web Roles".
- """
- if self.classname == 'user':
- if props.has_key('roles') and not self.hasPermission('Web Roles'):
- raise Unauthorised, _("You do not have permission to edit user roles")
- if self.isEditingSelf():
- return 1
- if self.hasPermission('Edit'):
- return 1
- return 0
-
- def newItemPermission(self, props):
- """Determine whether the user has permission to create (edit) this item.
-
- Base behaviour is to check the user can edit this class. No additional
- property checks are made. Additionally, new user items may be created
- if the user has the "Web Registration" Permission.
+ self.client.ok_message.append(self._('Items edited OK'))
- """
- if (self.classname == 'user' and self.hasPermission('Web Registration')
- or self.hasPermission('Edit')):
- return 1
- return 0
+class EditCommon(Action):
+ '''Utility methods for editing.'''
- #
- # Utility methods for editing
- #
- def _editnodes(self, all_props, all_links, newids=None):
+ def _editnodes(self, all_props, all_links):
''' Use the props in all_props to perform edit and creation, then
use the link specs in all_links to do linking.
'''
deps = {}
links = {}
for cn, nodeid, propname, vlist in all_links:
- if not all_props.has_key((cn, nodeid)):
+ numeric_id = int (nodeid or 0)
+ if not (numeric_id > 0 or (cn, nodeid) in all_props):
# link item to link to doesn't (and won't) exist
continue
+
for value in vlist:
- if not all_props.has_key(value):
+ if value not in all_props:
# link item to link to doesn't (and won't) exist
continue
deps.setdefault((cn, nodeid), []).append(value)
# loop detection
change = 0
while len(all_props) != len(done):
- for needed in all_props.keys():
- if done.has_key(needed):
+ for needed in all_props:
+ if needed in done:
continue
tlist = deps.get(needed, [])
for target in tlist:
- if not done.has_key(target):
+ if target not in done:
break
else:
done[needed] = 1
order.append(needed)
change = 1
if not change:
- raise ValueError, 'linking must not loop!'
+ raise ValueError('linking must not loop!')
# now, edit / create
m = []
for needed in order:
props = all_props[needed]
- if not props:
- # nothing to do
- continue
cn, nodeid = needed
-
- if nodeid is not None and int(nodeid) > 0:
- # make changes to the node
- props = self._changenode(cn, nodeid, props)
-
- # and some nice feedback for the user
- if props:
- info = ', '.join(props.keys())
- m.append('%s %s %s edited ok'%(cn, nodeid, info))
+ if props:
+ if nodeid is not None and int(nodeid) > 0:
+ # make changes to the node
+ props = self._changenode(cn, nodeid, props)
+
+ # and some nice feedback for the user
+ if props:
+ info = ', '.join(map(self._, props))
+ m.append(
+ self._('%(class)s %(id)s %(properties)s edited ok')
+ % {'class':cn, 'id':nodeid, 'properties':info})
+ else:
+ m.append(self._('%(class)s %(id)s - nothing changed')
+ % {'class':cn, 'id':nodeid})
else:
- m.append('%s %s - nothing changed'%(cn, nodeid))
- else:
- assert props
+ assert props
- # make a new node
- newid = self._createnode(cn, props)
- if nodeid is None:
- self.nodeid = newid
- nodeid = newid
+ # make a new node
+ newid = self._createnode(cn, props)
+ if nodeid is None:
+ self.nodeid = newid
+ nodeid = newid
- # and some nice feedback for the user
- m.append('%s %s created'%(cn, newid))
+ # and some nice feedback for the user
+ m.append(self._('%(class)s %(id)s created')
+ % {'class':cn, 'id':newid})
# fill in new ids in links
- if links.has_key(needed):
+ if needed in links:
for linkcn, linkid, linkprop in links[needed]:
props = all_props[(linkcn, linkid)]
cl = self.db.classes[linkcn]
propdef = cl.getprops()[linkprop]
- if not props.has_key(linkprop):
+ if linkprop not in props:
if linkid is None or linkid.startswith('-'):
# linking to a new item
if isinstance(propdef, hyperdb.Multilink):
- props[linkprop] = [newid]
+ props[linkprop] = [nodeid]
else:
- props[linkprop] = newid
+ props[linkprop] = nodeid
else:
# linking to an existing item
if isinstance(propdef, hyperdb.Multilink):
existing.append(nodeid)
props[linkprop] = existing
else:
- props[linkprop] = newid
+ props[linkprop] = nodeid
return '<br>'.join(m)
def _changenode(self, cn, nodeid, props):
"""Change the node based on the contents of the form."""
# check for permission
- if not self.editItemPermission(props):
- raise Unauthorised, 'You do not have permission to edit %s'%cn
+ if not self.editItemPermission(props, classname=cn, itemid=nodeid):
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to edit %(class)s'
+ ) % {'class': cn})
# make the changes
cl = self.db.classes[cn]
def _createnode(self, cn, props):
"""Create a node based on the contents of the form."""
# check for permission
- if not self.newItemPermission(props):
- raise Unauthorised, 'You do not have permission to create %s'%cn
+ if not self.newItemPermission(props, classname=cn):
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to create %(class)s'
+ ) % {'class': cn})
# create the node and return its id
cl = self.db.classes[cn]
return cl.create(**props)
-class EditItemAction(_EditAction):
+ def isEditingSelf(self):
+ """Check whether a user is editing his/her own details."""
+ return (self.nodeid == self.userid
+ and self.db.user.get(self.nodeid, 'username') != 'anonymous')
+
+ _cn_marker = []
+ def editItemPermission(self, props, classname=_cn_marker, itemid=None):
+ """Determine whether the user has permission to edit this item."""
+ if itemid is None:
+ itemid = self.nodeid
+ if classname is self._cn_marker:
+ classname = self.classname
+ # The user must have permission to edit each of the properties
+ # being changed.
+ for p in props:
+ if not self.hasPermission('Edit', itemid=itemid,
+ classname=classname, property=p):
+ return 0
+ # Since the user has permission to edit all of the properties,
+ # the edit is OK.
+ return 1
+
+ def newItemPermission(self, props, classname=None):
+ """Determine whether the user has permission to create this item.
+
+ Base behaviour is to check the user can edit this class. No additional
+ property checks are made.
+ """
+
+ if not classname :
+ classname = self.client.classname
+
+ if not self.hasPermission('Create', classname=classname):
+ return 0
+
+ # Check Create permission for each property, to avoid being able
+ # to set restricted ones on new item creation
+ for key in props:
+ if not self.hasPermission('Create', classname=classname,
+ property=key):
+ return 0
+ return 1
+
+class EditItemAction(EditCommon):
def lastUserActivity(self):
- if self.form.has_key(':lastactivity'):
- return date.Date(self.form[':lastactivity'].value)
- elif self.form.has_key('@lastactivity'):
- return date.Date(self.form['@lastactivity'].value)
+ if ':lastactivity' in self.form:
+ d = date.Date(self.form[':lastactivity'].value)
+ elif '@lastactivity' in self.form:
+ d = date.Date(self.form['@lastactivity'].value)
else:
return None
+ d.second = int(d.second)
+ return d
def lastNodeActivity(self):
cl = getattr(self.client.db, self.classname)
- return cl.get(self.nodeid, 'activity')
-
- def detectCollision(self, userActivity, nodeActivity):
- # Result from lastUserActivity may be None. If it is, assume there's no
- # conflict, or at least not one we can detect.
- if userActivity:
- return userActivity < nodeActivity
+ activity = cl.get(self.nodeid, 'activity').local(0)
+ activity.second = int(activity.second)
+ return activity
+
+ def detectCollision(self, user_activity, node_activity):
+ '''Check for a collision and return the list of props we edited
+ that conflict.'''
+ if user_activity and user_activity < node_activity:
+ props, links = self.client.parsePropsFromForm()
+ key = (self.classname, self.nodeid)
+ # we really only collide for direct prop edit conflicts
+ return list(props[key])
+ else:
+ return []
- def handleCollision(self):
- self.client.template = 'collision'
+ def handleCollision(self, props):
+ message = self._('Edit Error: someone else has edited this %s (%s). '
+ 'View <a target="new" href="%s%s">their changes</a> '
+ 'in a new window.')%(self.classname, ', '.join(props),
+ self.classname, self.nodeid)
+ self.client.error_message.append(message)
+ return
def handle(self):
"""Perform an edit of an item in the database.
See parsePropsFromForm and _editnodes for special variables.
"""
- if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
- self.handleCollision()
- return
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
+ user_activity = self.lastUserActivity()
+ if user_activity:
+ props = self.detectCollision(user_activity, self.lastNodeActivity())
+ if props:
+ self.handleCollision(props)
+ return
props, links = self.client.parsePropsFromForm()
# handle the props
try:
message = self._editnodes(props, links)
- except (ValueError, KeyError, IndexError, exceptions.Reject), message:
- self.client.error_message.append(_('Apply Error: ') + str(message))
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
+ self.client.error_message.append(
+ self._('Edit Error: %s') % str(message))
return
# commit now that all the tricky stuff is done
# we will want to include index-page args in this URL too
if self.nodeid is not None:
url += self.nodeid
- url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
- urllib.quote(self.template))
+ url += '?@ok_message=%s&@template=%s'%(urllib_.quote(message),
+ urllib_.quote(self.template))
if self.nodeid is None:
- req = templating.HTMLRequest(self)
- url += '&' + req.indexargs_href('', {})[1:]
- raise Redirect, url
+ req = templating.HTMLRequest(self.client)
+ url += '&' + req.indexargs_url('', {})[1:]
+ raise exceptions.Redirect(url)
-class NewItemAction(_EditAction):
+class NewItemAction(EditCommon):
def handle(self):
''' Add a new item to the database.
This follows the same form as the EditItemAction, with the same
special form values.
'''
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
# parse the props from the form
try:
props, links = self.client.parsePropsFromForm(create=1)
except (ValueError, KeyError), message:
- self.client.error_message.append(_('Error: ') + str(message))
+ self.client.error_message.append(self._('Error: %s')
+ % str(message))
return
# handle the props - edit or create
try:
# when it hits the None element, it'll set self.nodeid
messages = self._editnodes(props, links)
-
- except (ValueError, KeyError, IndexError, exceptions.Reject), message:
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
# these errors might just be indicative of user dumbness
- self.client.error_message.append(_('Error: ') + str(message))
+ self.client.error_message.append(_('Error: %s') % str(message))
return
# commit now that all the tricky stuff is done
self.db.commit()
# redirect to the new item's page
- raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
- self.classname, self.nodeid, urllib.quote(messages),
- urllib.quote(self.template))
+ raise exceptions.Redirect('%s%s%s?@ok_message=%s&@template=%s' % (
+ self.base, self.classname, self.nodeid, urllib_.quote(messages),
+ urllib_.quote(self.template)))
class PassResetAction(Action):
def handle(self):
"otk" performs the reset.
"""
- if self.form.has_key('otk'):
+ otks = self.db.getOTKManager()
+ if 'otk' in self.form:
# pull the rego information out of the otk database
otk = self.form['otk'].value
- otks = self.db.getOTKManager()
- uid = otks.get(otk, 'uid')
+ uid = otks.get(otk, 'uid', default=None)
if uid is None:
- self.client.error_message.append("""Invalid One Time Key!
-(a Mozilla bug may cause this message to show up erroneously,
- please check your email)""")
+ self.client.error_message.append(
+ self._("Invalid One Time Key!\n"
+ "(a Mozilla bug may cause this message "
+ "to show up erroneously, please check your email)"))
return
# re-open the database as "admin"
if self.user != 'admin':
self.client.opendb('admin')
self.db = self.client.db
+ otks = self.db.getOTKManager()
# change the password
newpw = password.generatePassword()
# XXX we need to make the "default" page be able to display errors!
try:
# set the password
- cl.set(uid, password=password.Password(newpw))
+ cl.set(uid, password=password.Password(newpw, config=self.db.config))
# clear the props from the otk database
otks.destroy(otk)
self.db.commit()
return
self.client.ok_message.append(
- 'Password reset and email sent to %s'%address)
+ self._('Password reset and email sent to %s') % address)
return
# no OTK, so now figure the user
- if self.form.has_key('username'):
+ if 'username' in self.form:
name = self.form['username'].value
try:
uid = self.db.user.lookup(name)
except KeyError:
- self.client.error_message.append('Unknown username')
+ self.client.error_message.append(self._('Unknown username'))
return
address = self.db.user.get(uid, 'address')
- elif self.form.has_key('address'):
+ elif 'address' in self.form:
address = self.form['address'].value
uid = uidFromAddress(self.db, ('', address), create=0)
if not uid:
- self.client.error_message.append('Unknown email address')
+ self.client.error_message.append(
+ self._('Unknown email address'))
return
name = self.db.user.get(uid, 'username')
else:
- self.client.error_message.append('You need to specify a username '
- 'or address')
+ self.client.error_message.append(
+ self._('You need to specify a username or address'))
return
# generate the one-time-key and store the props for later
if not self.client.standard_message([address], subject, body):
return
- self.client.ok_message.append('Email sent to %s'%address)
-
-class ConfRegoAction(Action):
- def handle(self):
- """Grab the OTK, use it to load up the new user details."""
- try:
- # pull the rego information out of the otk database
- self.userid = self.db.confirm_registration(self.form['otk'].value)
- except (ValueError, KeyError), message:
- self.client.error_message.append(str(message))
- return
+ self.client.ok_message.append(self._('Email sent to %s') % address)
+class RegoCommon(Action):
+ def finishRego(self):
# log the new user in
- self.client.user = self.db.user.get(self.userid, 'username')
+ self.client.userid = self.userid
+ user = self.client.user = self.db.user.get(self.userid, 'username')
# re-open the database for real, using the user
- self.client.opendb(self.client.user)
+ self.client.opendb(user)
- # if we have a session, update it
- if hasattr(self, 'session'):
- self.client.db.sessions.set(self.session, user=self.user,
- last_use=time.time())
- else:
- # new session cookie
- self.client.set_cookie(self.user)
+ # update session data
+ self.client.session_api.set(user=user)
# nice message
- message = _('You are now registered, welcome!')
+ message = self._('You are now registered, welcome!')
url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
- urllib.quote(message))
+ urllib_.quote(message))
# redirect to the user's page (but not 302, as some email clients seem
# to want to reload the page, or something)
window.setTimeout('window.location = "%s"', 1000);
</script>'''%(message, url, message, url)
-class RegisterAction(Action):
+class ConfRegoAction(RegoCommon):
+ def handle(self):
+ """Grab the OTK, use it to load up the new user details."""
+ try:
+ # pull the rego information out of the otk database
+ self.userid = self.db.confirm_registration(self.form['otk'].value)
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(str(message))
+ return
+ return self.finishRego()
+
+class RegisterAction(RegoCommon, EditCommon):
name = 'register'
- permissionType = 'Web Registration'
+ permissionType = 'Register'
def handle(self):
"""Attempt to create a new user based on the contents of the form
- and then set the cookie.
+ and then remember it in session.
Return 1 on successful login.
"""
- props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
-
- # registration isn't allowed to supply roles
- if props.has_key('roles'):
- raise Unauthorised, _("It is not permitted to supply roles "
- "at registration.")
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
- username = props['username']
+ # parse the props from the form
try:
- self.db.user.lookup(username)
- self.client.error_message.append(_('Error: A user with the '
- 'username "%(username)s" already exists')%props)
+ props, links = self.client.parsePropsFromForm(create=1)
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(self._('Error: %s')
+ % str(message))
return
- except KeyError:
- pass
+
+ # skip the confirmation step?
+ if self.db.config['INSTANT_REGISTRATION']:
+ # handle the create now
+ try:
+ # when it hits the None element, it'll set self.nodeid
+ messages = self._editnodes(props, links)
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
+ # these errors might just be indicative of user dumbness
+ self.client.error_message.append(_('Error: %s') % str(message))
+ return
+
+ # fix up the initial roles
+ self.db.user.set(self.nodeid,
+ roles=self.db.config['NEW_WEB_USER_ROLES'])
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # finish off by logging the user in
+ self.userid = self.nodeid
+ return self.finishRego()
# generate the one-time-key and store the props for later
- for propname, proptype in self.db.user.getprops().items():
- value = props.get(propname, None)
+ user_props = props[('user', None)]
+ for propname, proptype in self.db.user.getprops().iteritems():
+ value = user_props.get(propname, None)
if value is None:
pass
elif isinstance(proptype, hyperdb.Date):
- props[propname] = str(value)
+ user_props[propname] = str(value)
elif isinstance(proptype, hyperdb.Interval):
- props[propname] = str(value)
+ user_props[propname] = str(value)
elif isinstance(proptype, hyperdb.Password):
- props[propname] = str(value)
+ user_props[propname] = str(value)
otks = self.db.getOTKManager()
+ otk = ''.join([random.choice(chars) for x in range(32)])
while otks.exists(otk):
otk = ''.join([random.choice(chars) for x in range(32)])
- otks.set(otk, **props)
+ otks.set(otk, **user_props)
# send the email
tracker_name = self.db.config.TRACKER_NAME
tracker_email = self.db.config.TRACKER_EMAIL
- subject = 'Complete your registration to %s -- key %s'%(tracker_name,
+ if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
+ subject = 'Complete your registration to %s -- key %s'%(tracker_name,
otk)
- body = """To complete your registration of the user "%(name)s" with
+ body = """To complete your registration of the user "%(name)s" with
%(tracker)s, please do one of the following:
- send a reply to %(tracker_email)s and maintain the subject line as is (the
%(url)s?@action=confrego&otk=%(otk)s
-""" % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
- 'otk': otk, 'tracker_email': tracker_email}
- if not self.client.standard_message([props['address']], subject, body,
- tracker_email):
+""" % {'name': user_props['username'], 'tracker': tracker_name,
+ 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
+ else:
+ subject = 'Complete your registration to %s'%(tracker_name)
+ body = """To complete your registration of the user "%(name)s" with
+%(tracker)s, please visit the following URL:
+
+%(url)s?@action=confrego&otk=%(otk)s
+
+""" % {'name': user_props['username'], 'tracker': tracker_name,
+ 'url': self.base, 'otk': otk}
+ if not self.client.standard_message([user_props['address']], subject,
+ body, (tracker_name, tracker_email)):
return
# commit changes to the database
self.db.commit()
# redirect to the "you're almost there" page
- raise Redirect, '%suser?@template=rego_progress'%self.base
+ raise exceptions.Redirect('%suser?@template=rego_progress'%self.base)
+
+ def newItemPermission(self, props, classname=None):
+ """Just check the "Register" permission.
+ """
+ # registration isn't allowed to supply roles
+ if 'roles' in props:
+ raise exceptions.Unauthorised(self._(
+ "It is not permitted to supply roles at registration."))
+
+ # technically already checked, but here for clarity
+ return self.hasPermission('Register', classname=classname)
class LogoutAction(Action):
def handle(self):
- """Make us really anonymous - nuke the cookie too."""
+ """Make us really anonymous - nuke the session too."""
# log us out
self.client.make_user_anonymous()
-
- # construct the logout cookie
- now = Cookie._getdate()
- self.client.additional_headers['Set-Cookie'] = \
- '%s=deleted; Max-Age=0; expires=%s; Path=%s;'%(self.client.cookie_name,
- now, self.client.cookie_path)
+ self.client.session_api.destroy()
# Let the user know what's going on
- self.client.ok_message.append(_('You are logged out'))
+ self.client.ok_message.append(self._('You are logged out'))
+
+ # reset client context to render tracker home page
+ # instead of last viewed page (may be inaccessibe for anonymous)
+ self.client.classname = None
+ self.client.nodeid = None
+ self.client.template = None
class LoginAction(Action):
def handle(self):
Sets up a session for the user which contains the login credentials.
"""
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
# we need the username at a minimum
- if not self.form.has_key('__login_name'):
- self.client.error_message.append(_('Username required'))
+ if '__login_name' not in self.form:
+ self.client.error_message.append(self._('Username required'))
return
# get the login info
self.client.user = self.form['__login_name'].value
- if self.form.has_key('__login_password'):
+ if '__login_password' in self.form:
password = self.form['__login_password'].value
else:
password = ''
- # make sure the user exists
try:
- self.client.userid = self.db.user.lookup(self.client.user)
- except KeyError:
- name = self.client.user
- self.client.error_message.append(_('No such user "%(name)s"')%locals())
+ self.verifyLogin(self.client.user, password)
+ except exceptions.LoginError, err:
self.client.make_user_anonymous()
+ self.client.error_message.extend(list(err.args))
return
+ # now we're OK, re-open the database for real, using the user
+ self.client.opendb(self.client.user)
+
+ # save user in session
+ self.client.session_api.set(user=self.client.user)
+ if 'remember' in self.form:
+ self.client.session_api.update(set_cookie=True, expire=24*3600*365)
+
+ # If we came from someplace, go back there
+ if '__came_from' in self.form:
+ raise exceptions.Redirect(self.form['__came_from'].value)
+
+ def verifyLogin(self, username, password):
+ # make sure the user exists
+ try:
+ self.client.userid = self.db.user.lookup(username)
+ except KeyError:
+ raise exceptions.LoginError(self._('Invalid login'))
+
# verify the password
if not self.verifyPassword(self.client.userid, password):
- self.client.make_user_anonymous()
- self.client.error_message.append(_('Incorrect password'))
- return
+ raise exceptions.LoginError(self._('Invalid login'))
# Determine whether the user has permission to log in.
# Base behaviour is to check the user has "Web Access".
if not self.hasPermission("Web Access"):
- self.client.make_user_anonymous()
- self.client.error_message.append(_("You do not have permission to login"))
- return
+ raise exceptions.LoginError(self._(
+ "You do not have permission to login"))
- # now we're OK, re-open the database for real, using the user
- self.client.opendb(self.client.user)
-
- # set the session cookie
- self.client.set_cookie(self.client.user)
-
- def verifyPassword(self, userid, password):
- ''' Verify the password that the user has supplied
+ def verifyPassword(self, userid, givenpw):
+ '''Verify the password that the user has supplied.
+ Optionally migrate to new password scheme if configured
'''
- stored = self.db.user.get(self.client.userid, 'password')
- if password == stored:
+ db = self.db
+ stored = db.user.get(userid, 'password')
+ if givenpw == stored:
+ if db.config.WEB_MIGRATE_PASSWORDS and stored.needs_migration():
+ newpw = password.Password(givenpw, config=db.config)
+ db.user.set(userid, password=newpw)
+ db.commit()
return 1
- if not password and not stored:
+ if not givenpw and not stored:
return 1
return 0
def handle(self):
''' Export the specified search query as CSV. '''
# figure the request
- request = HTMLRequest(self)
+ request = templating.HTMLRequest(self.client)
filterspec = request.filterspec
sort = request.sort
group = request.group
else:
matches = None
- h = self.additional_headers
- h['Content-Type'] = 'text/csv'
+ h = self.client.additional_headers
+ h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
# some browsers will honor the filename here...
h['Content-Disposition'] = 'inline; filename=query.csv'
- self.header()
- writer = rcsv.writer(self.request.wfile)
- writer.writerow(columns)
+
+ self.client.header()
+
+ if self.client.env['REQUEST_METHOD'] == 'HEAD':
+ # all done, return a dummy string
+ return 'dummy'
+
+ wfile = self.client.request.wfile
+ if self.client.charset != self.client.STORAGE_CHARSET:
+ wfile = codecs.EncodedFile(wfile,
+ self.client.STORAGE_CHARSET, self.client.charset, 'replace')
+
+ writer = csv.writer(wfile)
+ self.client._socket_op(writer.writerow, columns)
# and search
for itemid in klass.filter(matches, filterspec, sort, group):
- writer.writerow([str(klass.get(itemid, col)) for col in columns])
+ row = []
+ for name in columns:
+ # check permission to view this property on this item
+ if not self.hasPermission('View', itemid=itemid,
+ classname=request.classname, property=name):
+ raise exceptions.Unauthorised(self._(
+ 'You do not have permission to view %(class)s'
+ ) % {'class': request.classname})
+ row.append(str(klass.get(itemid, name)))
+ self.client._socket_op(writer.writerow, row)
return '\n'
-# vim: set filetype=python ts=4 sw=4 et si
+
+class Bridge(BaseAction):
+ """Make roundup.actions.Action executable via CGI request.
+
+ Using this allows users to write actions executable from multiple frontends.
+ CGI Form content is translated into a dictionary, which then is passed as
+ argument to 'handle()'. XMLRPC requests have to pass this dictionary
+ directly.
+ """
+
+ def __init__(self, *args):
+
+ # As this constructor is callable from multiple frontends, each with
+ # different Action interfaces, we have to look at the arguments to
+ # figure out how to complete construction.
+ if (len(args) == 1 and
+ hasattr(args[0], '__class__') and
+ args[0].__class__.__name__ == 'Client'):
+ self.cgi = True
+ self.execute = self.execute_cgi
+ self.client = args[0]
+ self.form = self.client.form
+ else:
+ self.cgi = False
+
+ def execute_cgi(self):
+ args = {}
+ for key in self.form:
+ args[key] = self.form.getvalue(key)
+ self.permission(args)
+ return self.handle(args)
+
+ def permission(self, args):
+ """Raise Unauthorised if the current user is not allowed to execute
+ this action. Users may override this method."""
+
+ pass
+
+ def handle(self, args):
+
+ raise NotImplementedError
+
+# vim: set filetype=python sts=4 sw=4 et si :