diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index ac43e4c0a91fe1164b6e1379bf01c695607738db..d48fb595eb75514bd47c74a4bd8abbbfefec8466 100755 (executable)
--- a/roundup/cgi/actions.py
+++ b/roundup/cgi/actions.py
-#$Id: actions.py,v 1.73 2008-08-18 05:04:01 richard Exp $
-
-import re, cgi, StringIO, urllib, time, random, csv, codecs
+import re, cgi, time, random, csv, codecs
from roundup import hyperdb, token, date, password
+from roundup.actions import Action as BaseAction
from roundup.i18n import _
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',
if (self.permissionType and
not self.hasPermission(self.permissionType)):
info = {'action': self.name, 'classname': self.classname}
- raise exceptions.Unauthorised, self._(
+ raise exceptions.Unauthorised(self._(
'You do not have permission to '
- '%(action)s the %(classname)s class.')%info
+ '%(action)s the %(classname)s class.')%info)
_marker = []
- def hasPermission(self, permission, classname=_marker, itemid=None):
+ 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,
- classname=classname, itemid=itemid)
+ classname=classname, itemid=itemid, property=property)
def gettext(self, msgid):
"""Return the localized translation of msgid"""
def handle(self):
"""Show a node of a particular class/id."""
t = n = ''
- for key in self.form.keys():
+ for key in self.form:
if self.typere.match(key):
t = self.form[key].value.strip()
elif self.numre.match(key):
n = self.form[key].value.strip()
if not t:
- raise ValueError, self._('No type specified')
+ raise ValueError(self._('No type specified'))
if not n:
- raise exceptions.SeriousError, self._('No ID entered')
+ raise exceptions.SeriousError(self._('No ID entered'))
try:
int(n)
except ValueError:
d = {'input': n, 'classname': t}
- raise exceptions.SeriousError, self._(
- '"%(input)s" is not an ID (%(classname)s ID required)')%d
+ 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
+ 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, self._(
- '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(
self._('%(classname)s %(itemid)s has been retired')%{
- 'classname': self.classname.capitalize(), 'itemid': nodeid})
+ 'classname': self.classname.capitalize(), 'itemid': itemid})
- def hasPermission(self, permission, classname=Action._marker, itemid=None):
- if itemid is None:
- itemid = self.nodeid
- return Action.hasPermission(self, permission, classname, itemid)
class SearchAction(Action):
name = 'search'
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")
+ 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")
+ 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:
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")
+ 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")
+ 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)
def fakeFilterVars(self):
"""Add a faked :filter form variable for each filtering prop."""
cls = self.db.classes[self.classname]
- for key in self.form.keys():
+ for key in self.form:
prop = cls.get_transitive_prop(key)
if not prop:
continue
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:
def getFromForm(self, name):
for key in ('@' + name, ':' + name):
- if self.form.has_key(key):
+ if key in self.form:
return self.form[key].value.strip()
return ''
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.
-
"""
+ # 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)
+ rows = io_.StringIO(self.form['rows'].value)
reader = csv.reader(rows)
found = {}
line = 0
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(
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()
links = {}
for cn, nodeid, propname, vlist in all_links:
numeric_id = int (nodeid or 0)
- if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
+ 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 = []
# and some nice feedback for the user
if props:
- info = ', '.join(map(self._, props.keys()))
+ info = ', '.join(map(self._, props))
m.append(
self._('%(class)s %(id)s %(properties)s edited ok')
% {'class':cn, 'id':nodeid, 'properties':info})
% {'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)
"""Change the node based on the contents of the form."""
# check for permission
if not self.editItemPermission(props, classname=cn, itemid=nodeid):
- raise exceptions.Unauthorised, self._(
+ raise exceptions.Unauthorised(self._(
'You do not have permission to edit %(class)s'
- ) % {'class': cn}
+ ) % {'class': cn})
# make the changes
cl = self.db.classes[cn]
"""Create a node based on the contents of the form."""
# check for permission
if not self.newItemPermission(props, classname=cn):
- raise exceptions.Unauthorised, self._(
+ raise exceptions.Unauthorised(self._(
'You do not have permission to create %(class)s'
- ) % {'class': cn}
+ ) % {'class': cn})
# create the node and return its id
cl = self.db.classes[cn]
_cn_marker = []
def editItemPermission(self, props, classname=_cn_marker, itemid=None):
- """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 exceptions.Unauthorised, self._(
- "You do not have permission to edit user roles")
- if self.isEditingSelf():
- return 1
+ """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
- if self.hasPermission('Edit', itemid=itemid, classname=classname):
- return 1
- return 0
+ # 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
- return self.hasPermission('Create', classname=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'):
+ if ':lastactivity' in self.form:
d = date.Date(self.form[':lastactivity'].value)
- elif self.form.has_key('@lastactivity'):
+ elif '@lastactivity' in self.form:
d = date.Date(self.form['@lastactivity'].value)
else:
return None
props, links = self.client.parsePropsFromForm()
key = (self.classname, self.nodeid)
# we really only collide for direct prop edit conflicts
- return props[key].keys()
+ return list(props[key])
else:
return []
See parsePropsFromForm and _editnodes for special variables.
"""
+ # 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())
# 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.client)
url += '&' + req.indexargs_url('', {})[1:]
- raise exceptions.Redirect, url
+ raise exceptions.Redirect(url)
class NewItemAction(EditCommon):
def handle(self):
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)
self.db.commit()
# redirect to the new item's page
- raise exceptions.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):
"""
otks = self.db.getOTKManager()
- if self.form.has_key('otk'):
+ if 'otk' in self.form:
# pull the rego information out of the otk database
otk = self.form['otk'].value
uid = otks.get(otk, 'uid', default=None)
# 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
# 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)
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:
# nice message
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)
class RegisterAction(RegoCommon, EditCommon):
name = 'register'
- permissionType = 'Create'
+ permissionType = 'Register'
def handle(self):
"""Attempt to create a new user based on the contents of the form
Return 1 on successful login.
"""
+ # 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)
% str(message))
return
- # registration isn't allowed to supply roles
- user_props = props[('user', None)]
- if user_props.has_key('roles'):
- raise exceptions.Unauthorised, self._(
- "It is not permitted to supply roles at registration.")
-
# skip the confirmation step?
if self.db.config['INSTANT_REGISTRATION']:
# handle the create now
return self.finishRego()
# generate the one-time-key and store the props for later
- for propname, proptype in self.db.user.getprops().items():
+ 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
self.db.commit()
# redirect to the "you're almost there" page
- raise exceptions.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):
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'):
+ 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 = ''
# save user in session
self.client.session_api.set(user=self.client.user)
- if self.form.has_key('remember'):
+ 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 self.form.has_key('__came_from'):
- raise exceptions.Redirect, self.form['__came_from'].value
+ 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')
+ raise exceptions.LoginError(self._('Invalid login'))
# verify the password
if not self.verifyPassword(self.client.userid, password):
- raise exceptions.LoginError, self._('Invalid login')
+ 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"):
- raise exceptions.LoginError, self._(
- "You do not have permission to login")
+ raise exceptions.LoginError(self._(
+ "You do not have permission to login"))
- def verifyPassword(self, userid, password):
- '''Verify the password that the user has supplied'''
- stored = self.db.user.get(userid, 'password')
- if password == stored:
+ def verifyPassword(self, userid, givenpw):
+ '''Verify the password that the user has supplied.
+ Optionally migrate to new password scheme if configured
+ '''
+ 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
# and search
for itemid in klass.filter(matches, filterspec, sort, group):
- self.client._socket_op(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'
+
+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 :