diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index 136fb995eae523d8ddd398465249eab8547dd248..36dfee3707f96a02c0c12c0cd49c70f66c8f718c 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
from roundup import hyperdb, token, date, password
import re, cgi, StringIO, urllib, 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.i18n import _
import roundup.exceptions
from roundup.cgi import exceptions, templating
def handle(self):
"""Retire the context item."""
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)
# 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 \
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'):
+ self.db.user.get(itemid, 'username') in ('admin', 'anonymous'):
raise ValueError, self._(
'You may not retire the admin or anonymous user')
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
# 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')%{
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'
class SearchAction(Action):
name = 'search'
if isinstance(prop, hyperdb.String):
v = self.form[key].value
l = token.token_split(v)
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.remove(self.form[key])
# replace the single value with the split list
for v in l:
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.
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]
cl = self.db.classes[self.classname]
- idlessprops = cl.getprops(protected=0).keys()
- idlessprops.sort()
- props = ['id'] + idlessprops
+ props_without_id = cl.getprops(protected=0).keys()
+
+ # 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)
# do the edit
rows = StringIO.StringIO(self.form['rows'].value)
if values == props:
continue
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
# 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
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
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 = {}
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
prop = cl.properties[name]
value = value.strip()
# only add the property if it has a value
# perform the edit
if exists:
# edit existing
# 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
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 not found.has_key(itemid):
+ # 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()
# all OK
self.db.commit()
if linkid is None or linkid.startswith('-'):
# linking to a new item
if isinstance(propdef, hyperdb.Multilink):
if linkid is None or linkid.startswith('-'):
# linking to a new item
if isinstance(propdef, hyperdb.Multilink):
- props[linkprop] = [newid]
+ props[linkprop] = [nodeid]
else:
else:
- props[linkprop] = newid
+ props[linkprop] = nodeid
else:
# linking to an existing item
if isinstance(propdef, hyperdb.Multilink):
else:
# linking to an existing item
if isinstance(propdef, hyperdb.Multilink):
existing.append(nodeid)
props[linkprop] = existing
else:
existing.append(nodeid)
props[linkprop] = existing
else:
- props[linkprop] = newid
+ props[linkprop] = nodeid
return '<br>'.join(m)
return '<br>'.join(m)
# The user must have permission to edit each of the properties
# being changed.
for p in props:
# 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.
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.
"""
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 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):
class EditItemAction(EditCommon):
def lastUserActivity(self):
See parsePropsFromForm and _editnodes for special variables.
"""
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())
user_activity = self.lastUserActivity()
if user_activity:
props = self.detectCollision(user_activity, self.lastNodeActivity())
This follows the same form as the EditItemAction, with the same
special form values.
'''
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)
# parse the props from the form
try:
props, links = self.client.parsePropsFromForm(create=1)
class RegisterAction(RegoCommon, EditCommon):
name = 'register'
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
def handle(self):
"""Attempt to create a new user based on the contents of the form
Return 1 on successful login.
"""
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)
# parse the props from the form
try:
props, links = self.client.parsePropsFromForm(create=1)
Sets up a session for the user which contains the login credentials.
"""
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(self._('Username required'))
# we need the username at a minimum
if not self.form.has_key('__login_name'):
self.client.error_message.append(self._('Username required'))
# and search
for itemid in klass.filter(matches, filterspec, sort, group):
# 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'
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.keys():
+ 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 :
# vim: set filetype=python sts=4 sw=4 et si :