15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# Author: Matt Giuca, Will Grant
18
# Author: Matt Giuca, Will Grant, Nick Chadwick
26
import genshi.template
25
28
from ivle.webapp.base.views import BaseView
26
from ivle.webapp.errors import BadRequest, MethodNotAllowed
29
from ivle.webapp.errors import BadRequest, MethodNotAllowed, Unauthorized
28
31
class RESTView(BaseView):
33
36
content_type = "application/octet-stream"
35
38
def __init__(self, req, *args, **kwargs):
40
setattr(self, key, kwargs[key])
38
42
def render(self, req):
39
if req.method == 'GET':
40
outstr = self.GET(req)
42
if req.method == 'PUT':
43
outstr = self.PATCH(req, req.read())
44
req.content_type = self.content_type
43
raise NotImplementedError()
47
45
class JSONRESTView(RESTView):
54
52
lambda self: [m for m in ('GET', 'PUT', 'PATCH')
55
53
if hasattr(self, m)] + ['POST'])
55
def authorize(self, req):
56
return True # Real authz performed in render().
58
def authorize_method(self, req, op):
59
if not hasattr(op, '_rest_api_permission'):
62
if op._rest_api_permission not in self.get_permissions(req.user):
65
def convert_bool(self, value):
66
if value in ('True', 'true', True):
68
elif value in ('False', 'false', False):
57
73
def render(self, req):
58
74
if req.method not in self._allowed_methods:
59
75
raise MethodNotAllowed(allowed=self._allowed_methods)
61
77
if req.method == 'GET':
78
self.authorize_method(req, self.GET)
62
79
outjson = self.GET(req)
63
80
# Since PATCH isn't yet an official HTTP method, we allow users to
64
81
# turn a PUT into a PATCH by supplying a special header.
65
82
elif req.method == 'PATCH' or (req.method == 'PUT' and
66
83
'X-IVLE-Patch-Semantics' in req.headers_in and
67
84
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
68
outjson = self.PATCH(req, cjson.decode(req.read()))
85
self.authorize_method(req, self.PATCH)
87
input = cjson.decode(req.read())
88
except cjson.DecodeError:
89
raise BadRequest('Invalid JSON data')
90
outjson = self.PATCH(req, input)
69
91
elif req.method == 'PUT':
70
outjson = self.PUT(req, cjson.decode(req.read()))
92
self.authorize_method(req, self.PUT)
94
input = cjson.decode(req.read())
95
except cjson.DecodeError:
96
raise BadRequest('Invalid JSON data')
97
outjson = self.PUT(req, input)
71
98
# POST implies named operation.
72
99
elif req.method == 'POST':
73
100
# TODO: Check Content-Type and implement multipart/form-data.
74
opargs = dict(cgi.parse_qsl(req.read()))
102
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
76
104
opname = opargs['ivle.op']
77
105
del opargs['ivle.op']
87
115
not op._rest_api_callable:
88
116
raise BadRequest('Invalid named operation.')
118
self.authorize_method(req, op)
90
120
# Find any missing arguments, except for the first two (self, req)
91
121
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
98
128
unspec = set(args) - set(opargs.keys())
99
129
if unspec and not defaults:
100
raise BadRequest('Missing arguments: ' + ','.join(unspec))
130
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
102
132
unspec = [k for k in unspec if k not in args[-len(defaults):]]
105
raise BadRequest('Missing arguments: ' + ','.join(unspec))
135
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
107
137
# We have extra arguments if the are no match args in the function
108
138
# signature, AND there is no **.
111
141
raise BadRequest('Extra arguments: ' + ', '.join(extra))
113
143
outjson = op(req, **opargs)
115
raise AssertionError('Unknown method somehow got through.')
117
145
req.content_type = self.content_type
146
self.write_json(req, outjson)
148
#This is a separate function to allow additional data to be passed through
149
def write_json(self, req, outjson):
118
150
if outjson is not None:
119
151
req.write(cjson.encode(outjson))
122
def named_operation(meth):
155
class XHTMLRESTView(JSONRESTView):
156
"""A special type of RESTView which takes enhances the standard JSON
157
with genshi XHTML functions.
159
XHTMLRESTViews should have a template, which is rendered using their
160
context. This is returned in the JSON as 'html'"""
162
ctx = genshi.template.Context()
164
def __init__(self, req, *args, **kwargs):
166
setattr(self, key, kwargs[key])
168
def render_fragment(self):
169
if self.template is None:
170
raise NotImplementedError()
172
rest_template = os.path.join(os.path.dirname(
173
inspect.getmodule(self).__file__), self.template)
174
loader = genshi.template.TemplateLoader(".", auto_reload=True)
175
tmpl = loader.load(rest_template)
177
return tmpl.generate(self.ctx).render('xhtml', doctype='xhtml')
179
# This renders the template and adds it to the json
180
def write_json(self, req, outjson):
181
outjson["html"] = self.render_fragment()
182
req.write(cjson.encode(outjson))
185
class named_operation(object):
123
186
'''Declare a function to be accessible to HTTP users via the REST API.
125
meth._rest_api_callable = True
188
def __init__(self, permission):
189
self.permission = permission
191
def __call__(self, func):
192
func._rest_api_callable = True
193
func._rest_api_permission = self.permission
196
class require_permission(object):
197
'''Declare the permission required for use of a method via the REST API.
199
def __init__(self, permission):
200
self.permission = permission
202
def __call__(self, func):
203
func._rest_api_permission = self.permission