diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index 4f5a0b00c9dec3f7d41efccc67844f0efc2f60a5..41f5979467dee8b5dceed43bdc0cd8298dd08fcf 100755 (executable)
--- a/roundup/cgi/actions.py
+++ b/roundup/cgi/actions.py
-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
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, property=None):
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
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
# 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]
# 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):
+ 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.
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)
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():
+ db.user.set(userid, password=password.Password(givenpw))
+ 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'
def execute_cgi(self):
args = {}
- for key in self.form.keys():
+ 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 :