diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index 994a96f5aa681b889b7bfdab198a2807f816fc22..33e7281399c16268aa20d34f75ae2ac60994828f 100755 (executable)
--- a/roundup/cgi/actions.py
+++ b/roundup/cgi/actions.py
from roundup import hyperdb, token, date, password, rcsv
from roundup.i18n import _
from roundup.cgi import templating
-from roundup.cgi.exceptions import Redirect, Unauthorised
+from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
from roundup.mailgw import uidFromAddress
__all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
'EditCSVAction', 'EditItemAction', 'PassResetAction',
- 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction']
+ 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
+ 'NewItemAction']
# used by a couple of routines
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
self.userid = client.userid
self.base = client.base
self.user = client.user
-
- def handle(self):
+
+ def execute(self):
"""Execute the action specified by this object."""
- raise NotImplementedError
+ self.permission()
+ self.handle()
+ name = ''
+ permissionType = None
def permission(self):
"""Check whether the user has permission to execute this action.
- True by default.
+ True by default. If the permissionType attribute is a string containing
+ a simple permission, check whether the user has that permission.
+ Subclasses must also define the name attribute if they define
+ permissionType.
+
+ Despite having this permission, users may still be unauthorised to
+ perform parts of actions. It is up to the subclasses to detect this.
"""
- return 1
+ 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
+
+ def hasPermission(self, permission):
+ """Check whether the user has 'permission' on the current class."""
+ return self.db.security.hasPermission(permission, self.client.userid,
+ self.client.classname)
class ShowAction(Action):
def handle(self, typere=re.compile('[@:]type'),
elif numre.match(key):
n = self.form[key].value.strip()
if not t:
- raise ValueError, 'Invalid %s number'%t
+ raise ValueError, 'No type specified'
+ if not n:
+ raise SeriousError, _('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
class RetireAction(Action):
+ name = 'retire'
+ permissionType = 'Edit'
+
def handle(self):
"""Retire the context item."""
# if we want to view the index template now, then unset the nodeid
if self.template == 'index':
self.client.nodeid = None
- # generic edit is per-class only
- if not self.permission():
- raise Unauthorised, _('You do not have permission to retire %s' %
- self.classname)
-
# 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'):
_('%(classname)s %(itemid)s has been retired')%{
'classname': self.classname.capitalize(), 'itemid': nodeid})
- def permission(self):
- """Determine whether the user has permission to retire this class.
-
- Base behaviour is to check the user can edit this class.
- """
- return self.db.security.hasPermission('Edit', self.client.userid,
- self.client.classname)
-
class SearchAction(Action):
+ name = 'search'
+ permissionType = 'View'
+
def handle(self, wcre=re.compile(r'[\s,]+')):
"""Mangle some of the form variables.
Split any String query values on whitespace and comma.
"""
- # generic edit is per-class only
- if not self.permission():
- raise Unauthorised, _('You do not have permission to search %s' %
- self.classname)
-
self.fakeFilterVars()
- queryname = self.getQueryName()
+ queryname = self.getQueryName()
# handle saving the query params
if queryname:
# and add it to the user's query multilink
queries = self.db.user.get(self.userid, 'queries')
- queries.append(qid)
- self.db.user.set(self.userid, queries=queries)
+ if qid not in queries:
+ queries.append(qid)
+ self.db.user.set(self.userid, queries=queries)
# commit the query change to the database
self.db.commit()
# replace the single value with the split list
for v in l:
self.form.value.append(cgi.MiniFieldStorage(key, v))
-
+
self.form.value.append(cgi.MiniFieldStorage('@filter', key))
FV_QUERYNAME = re.compile(r'[@:]queryname')
if self.FV_QUERYNAME.match(key):
return self.form[key].value.strip()
return ''
-
- def permission(self):
- return self.db.security.hasPermission('View', self.client.userid,
- self.client.classname)
class EditCSVAction(Action):
+ name = 'edit'
+ permissionType = 'Edit'
+
def handle(self):
"""Performs an edit of all of a class' items in one go.
removed lines are retired.
"""
- # this is per-class only
- if not self.permission():
- self.client.error_message.append(
- _('You do not have permission to edit %s' %self.classname))
- return
-
# get the CSV module
if rcsv.error:
self.client.error_message.append(_(rcsv.error))
self.client.ok_message.append(_('Items edited OK'))
- def permission(self):
- return self.db.security.hasPermission('Edit', self.client.userid,
- self.client.classname)
-
-class EditItemAction(Action):
- def handle(self):
- """Perform an edit of an item in the database.
-
- See parsePropsFromForm and _editnodes for special variables.
-
- """
- props, links = self.client.parsePropsFromForm()
-
- # handle the props
- try:
- message = self._editnodes(props, links)
- except (ValueError, KeyError, IndexError), message:
- self.client.error_message.append(_('Apply Error: ') + str(message))
- return
+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')
- # commit now that all the tricky stuff is done
- self.db.commit()
-
- # redirect to the item's edit page
- raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
- self.classname, self.client.nodeid,
- urllib.quote(message),
- urllib.quote(self.template))
-
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.
+ 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 this is a user node and the user is editing their own node, then
- # we're OK
- has = self.db.security.hasPermission
if self.classname == 'user':
- # reject if someone's trying to edit "roles" and doesn't have the
- # right permission.
- if props.has_key('roles') and not has('Web Roles', self.userid,
- 'user'):
- return 0
- # if the item being edited is the current user, we're ok
- if (self.nodeid == self.userid
- and self.db.user.get(self.nodeid, 'username') != 'anonymous'):
+ 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.db.security.hasPermission('Edit', self.userid, self.classname):
+ if self.hasPermission('Edit'):
return 1
return 0
if the user has the "Web Registration" Permission.
"""
- has = self.db.security.hasPermission
- if self.classname == 'user' and has('Web Registration', self.userid,
- 'user'):
- return 1
- if has('Edit', self.userid, self.classname):
+ if (self.classname == 'user' and self.hasPermission('Web Registration')
+ or self.hasPermission('Edit')):
return 1
return 0
# make a new node
newid = self._createnode(cn, props)
if nodeid is None:
- self.client.nodeid = newid
+ self.nodeid = newid
nodeid = newid
# and some nice feedback for the user
# create the node and return its id
cl = self.db.classes[cn]
return cl.create(**props)
-
+
+class EditItemAction(_EditAction):
+ 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)
+ else:
+ return None
+
+ 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
+
+ def handleCollision(self):
+ self.client.template = 'collision'
+
+ 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
+
+ props, links = self.client.parsePropsFromForm()
+
+ # handle the props
+ try:
+ message = self._editnodes(props, links)
+ except (ValueError, KeyError, IndexError), message:
+ self.client.error_message.append(_('Apply Error: ') + str(message))
+ return
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # redirect to the item's edit page
+ # redirect to finish off
+ url = self.base + self.classname
+ # note that this action might have been called by an index page, so
+ # 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))
+ if self.nodeid is None:
+ req = templating.HTMLRequest(self)
+ url += '&' + req.indexargs_href('', {})[1:]
+ raise Redirect, url
+
+class NewItemAction(_EditAction):
+ def handle(self):
+ ''' Add a new item to the database.
+
+ This follows the same form as the EditItemAction, with the same
+ special form values.
+ '''
+ # 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))
+ 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), message:
+ # these errors might just be indicative of user dumbness
+ self.client.error_message.append(_('Error: ') + 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))
+
class PassResetAction(Action):
def handle(self):
"""Handle password reset requests.
-
+
Presence of either "name" or "address" generates email. Presence of
"otk" performs the reset.
-
+
"""
if self.form.has_key('otk'):
# pull the rego information out of the otk database
otk = self.form['otk'].value
- uid = self.db.otks.get(otk, 'uid')
+ otks = self.db.getOTKManager()
+ uid = otks.get(otk, 'uid')
if uid is None:
self.client.error_message.append("""Invalid One Time Key!
(a Mozilla bug may cause this message to show up erroneously,
newpw = password.generatePassword()
cl = self.db.user
-# XXX we need to make the "default" page be able to display errors!
+ # XXX we need to make the "default" page be able to display errors!
try:
# set the password
cl.set(uid, password=password.Password(newpw))
# clear the props from the otk database
- self.db.otks.destroy(otk)
+ otks.destroy(otk)
self.db.commit()
except (ValueError, KeyError), message:
self.client.error_message.append(str(message))
if not self.client.standard_message([address], subject, body):
return
- self.client.ok_message.append('Password reset and email sent to %s' %
- address)
+ self.client.ok_message.append(
+ 'Password reset and email sent to %s'%address)
return
# no OTK, so now figure the user
# generate the one-time-key and store the props for later
otk = ''.join([random.choice(chars) for x in range(32)])
- self.db.otks.set(otk, uid=uid, __time=time.time())
+ while otks.exists(otk):
+ otk = ''.join([random.choice(chars) for x in range(32)])
+ otks.set(otk, uid=uid)
+ self.db.commit()
# send the email
tracker_name = self.db.config.TRACKER_NAME
# pull the rego information out of the otk database
self.userid = self.db.confirm_registration(self.form['otk'].value)
except (ValueError, KeyError), message:
- # XXX: we need to make the "default" page be able to display errors!
self.client.error_message.append(str(message))
return
-
+
# log the new user in
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.db = client.db
# if we have a session, update it
if hasattr(self, 'session'):
- self.db.sessions.set(self.session, user=self.user,
+ self.client.db.sessions.set(self.session, user=self.user,
last_use=time.time())
else:
# new session cookie
# nice message
message = _('You are now registered, welcome!')
+ url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
+ urllib.quote(message))
- # redirect to the user's page
- raise Redirect, '%suser%s?@ok_message=%s'%(self.base,
- self.userid, 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)
+ return '''<html><head><title>%s</title></head>
+ <body><p><a href="%s">%s</a></p>
+ <script type="text/javascript">
+ window.setTimeout('window.location = "%s"', 1000);
+ </script>'''%(message, url, message, url)
class RegisterAction(Action):
+ name = 'register'
+ permissionType = 'Web Registration'
+
def handle(self):
"""Attempt to create a new user based on the contents of the form
and then set the cookie.
Return 1 on successful login.
"""
- props = self.client.parsePropsFromForm()[0][('user', None)]
+ props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
- # make sure we're allowed to register
- if not self.permission(props):
- raise Unauthorised, _("You do not have permission to register")
+ # registration isn't allowed to supply roles
+ if props.has_key('roles'):
+ raise Unauthorised, _("It is not permitted to supply roles "
+ "at registration.")
+ username = props['username']
try:
- self.db.user.lookup(props['username'])
- self.client.error_message.append('Error: A user with the username "%s" '
- 'already exists'%props['username'])
+ self.db.user.lookup(username)
+ self.client.error_message.append(_('Error: A user with the '
+ 'username "%(username)s" already exists')%props)
return
except KeyError:
pass
# generate the one-time-key and store the props for later
- otk = ''.join([random.choice(chars) for x in range(32)])
for propname, proptype in self.db.user.getprops().items():
value = props.get(propname, None)
if value is None:
props[propname] = str(value)
elif isinstance(proptype, hyperdb.Password):
props[propname] = str(value)
- props['__time'] = time.time()
- self.db.otks.set(otk, **props)
+ otks = self.db.getOTKManager()
+ while otks.exists(otk):
+ otk = ''.join([random.choice(chars) for x in range(32)])
+ otks.set(otk, **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,
+ subject = 'Complete your registration to %s -- key %s'%(tracker_name,
otk)
body = """To complete your registration of the user "%(name)s" with
%(tracker)s, please do one of the following:
# redirect to the "you're almost there" page
raise Redirect, '%suser?@template=rego_progress'%self.base
- def permission(self, props):
- """Determine whether the user has permission to register
-
- Base behaviour is to check the user has "Web Registration".
-
- """
- # registration isn't allowed to supply roles
- if props.has_key('roles'):
- return 0
- if self.db.security.hasPermission('Web Registration', self.userid):
- return 1
- return 0
-
class LogoutAction(Action):
def handle(self):
"""Make us really anonymous - nuke the cookie too."""
self.client.error_message.append(_('Incorrect password'))
return
- # make sure we're allowed to be here
- if not self.permission():
+ # 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
if not password and not stored:
return 1
return 0
-
- def permission(self):
- """Determine whether the user has permission to log in.
-
- Base behaviour is to check the user has "Web Access".
-
- """
- if not self.db.security.hasPermission('Web Access', self.client.userid):
- return 0
- return 1