1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
4 # This file is part of the Patchwork package.
6 # Patchwork is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # Patchwork is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Patchwork; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 # Patchwork XMLRPC interface
23 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
24 from django.http import HttpResponse, HttpResponseRedirect, \
25 HttpResponseServerError
26 from django.core import urlresolvers
27 from django.contrib.auth import authenticate
28 from patchwork.models import Patch, Project, Person, State
29 from django.views.decorators.csrf import csrf_exempt
35 class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher):
37 if sys.version_info[:3] >= (2,5,):
38 SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
40 def _dumps(obj, *args, **kwargs):
41 kwargs['allow_none'] = self.allow_none
42 kwargs['encoding'] = self.encoding
43 return xmlrpclib.dumps(obj, *args, **kwargs)
45 def _dumps(obj, *args, **kwargs):
46 return xmlrpclib.dumps(obj, *args, **kwargs)
47 SimpleXMLRPCDispatcher.__init__(self)
51 # map of name => (auth, func)
54 def register_function(self, fn, auth_required):
55 self.func_map[fn.__name__] = (auth_required, fn)
58 def _user_for_request(self, request):
61 if 'HTTP_AUTHORIZATION' in request.META:
62 auth_header = request.META.get('HTTP_AUTHORIZATION')
63 elif 'Authorization' in request.META:
64 auth_header = request.META.get('Authorization')
66 if auth_header is None or auth_header == '':
67 raise Exception("No authentication credentials given")
69 str = auth_header.strip()
71 if not str.startswith('Basic '):
72 raise Exception("Authentication scheme not supported")
74 str = str[len('Basic '):].strip()
77 decoded = base64.decodestring(str)
78 username, password = decoded.split(':', 1)
80 raise Exception("Invalid authentication credentials")
82 return authenticate(username = username, password = password)
85 def _dispatch(self, request, method, params):
86 if method not in self.func_map.keys():
87 raise Exception('method "%s" is not supported' % method)
89 auth_required, fn = self.func_map[method]
92 user = self._user_for_request(request)
94 raise Exception("Invalid username/password")
96 params = (user,) + params
100 def _marshaled_dispatch(self, request):
102 params, method = xmlrpclib.loads(request.raw_post_data)
104 response = self._dispatch(request, method, params)
105 # wrap response in a singleton tuple
106 response = (response,)
107 response = self.dumps(response, methodresponse=1)
108 except xmlrpclib.Fault, fault:
109 response = self.dumps(fault)
111 # report exception back to server
112 response = self.dumps(
113 xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)),
118 dispatcher = PatchworkXMLRPCDispatcher()
120 # XMLRPC view function
123 if request.method != 'POST':
124 return HttpResponseRedirect(
125 urlresolvers.reverse('patchwork.views.help',
126 kwargs = {'path': 'pwclient/'}))
128 response = HttpResponse()
130 ret = dispatcher._marshaled_dispatch(request)
133 return HttpResponseServerError()
137 # decorator for XMLRPC methods. Setting login_required to true will call
138 # the decorated function with a non-optional user as the first argument.
139 def xmlrpc_method(login_required = False):
141 dispatcher.register_function(f, login_required)
148 # We allow most of the Django field lookup types for remote queries
149 LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt",
150 "in", "startswith", "istartswith", "endswith",
151 "iendswith", "range", "year", "month", "day", "isnull" ]
153 #######################################################################
155 #######################################################################
157 def project_to_dict(obj):
158 """Return a trimmed down dictionary representation of a Project
159 object which is OK to send to the client."""
163 'linkname' : obj.linkname,
167 def person_to_dict(obj):
168 """Return a trimmed down dictionary representation of a Person
169 object which is OK to send to the client."""
171 # Make sure we don't return None even if the user submitted a patch
172 # with no real name. XMLRPC can't marshall None.
173 if obj.name is not None:
183 'user' : unicode(obj.user).encode("utf-8"),
186 def patch_to_dict(obj):
187 """Return a trimmed down dictionary representation of a Patch
188 object which is OK to send to the client."""
192 'date' : unicode(obj.date).encode("utf-8"),
193 'filename' : obj.filename(),
196 'project' : unicode(obj.project).encode("utf-8"),
197 'project_id' : obj.project_id,
198 'state' : unicode(obj.state).encode("utf-8"),
199 'state_id' : obj.state_id,
200 'submitter' : unicode(obj.submitter).encode("utf-8"),
201 'submitter_id' : obj.submitter_id,
202 'delegate' : unicode(obj.delegate).encode("utf-8"),
203 'delegate_id' : max(obj.delegate_id, 0),
204 'commit_ref' : max(obj.commit_ref, ''),
207 def bundle_to_dict(obj):
208 """Return a trimmed down dictionary representation of a Bundle
209 object which is OK to send to the client."""
214 'n_patches' : obj.n_patches(),
215 'public_url' : obj.public_url(),
218 def state_to_dict(obj):
219 """Return a trimmed down dictionary representation of a State
220 object which is OK to send to the client."""
227 #######################################################################
228 # Public XML-RPC methods
229 #######################################################################
231 @xmlrpc_method(False)
232 def pw_rpc_version():
233 """Return Patchwork XML-RPC interface version."""
236 @xmlrpc_method(False)
237 def project_list(search_str="", max_count=0):
238 """Get a list of projects matching the given filters."""
240 if len(search_str) > 0:
241 projects = Project.objects.filter(linkname__icontains = search_str)
243 projects = Project.objects.all()
246 return map(project_to_dict, projects)[:max_count]
248 return map(project_to_dict, projects)
252 @xmlrpc_method(False)
253 def project_get(project_id):
254 """Return structure for the given project ID."""
256 project = Project.objects.filter(id = project_id)[0]
257 return project_to_dict(project)
261 @xmlrpc_method(False)
262 def person_list(search_str="", max_count=0):
263 """Get a list of Person objects matching the given filters."""
265 if len(search_str) > 0:
266 people = (Person.objects.filter(name__icontains = search_str) |
267 Person.objects.filter(email__icontains = search_str))
269 people = Person.objects.all()
272 return map(person_to_dict, people)[:max_count]
274 return map(person_to_dict, people)
279 @xmlrpc_method(False)
280 def person_get(person_id):
281 """Return structure for the given person ID."""
283 person = Person.objects.filter(id = person_id)[0]
284 return person_to_dict(person)
288 @xmlrpc_method(False)
289 def patch_list(filter={}):
290 """Get a list of patches matching the given filters."""
292 # We allow access to many of the fields. But, some fields are
293 # filtered by raw object so we must lookup by ID instead over
313 parts = key.split("__")
314 if parts[0] not in ok_fields:
315 # Invalid field given
318 if LOOKUP_TYPES.count(parts[1]) == 0:
319 # Invalid lookup type given
322 if parts[0] == 'project_id':
323 dfilter['project'] = Project.objects.filter(id =
325 elif parts[0] == 'submitter_id':
326 dfilter['submitter'] = Person.objects.filter(id =
328 elif parts[0] == 'state_id':
329 dfilter['state'] = State.objects.filter(id =
331 elif parts[0] == 'max_count':
332 max_count = filter[key]
334 dfilter[key] = filter[key]
336 patches = Patch.objects.filter(**dfilter)
339 return map(patch_to_dict, patches[:max_count])
341 return map(patch_to_dict, patches)
346 @xmlrpc_method(False)
347 def patch_get(patch_id):
348 """Return structure for the given patch ID."""
350 patch = Patch.objects.filter(id = patch_id)[0]
351 return patch_to_dict(patch)
355 @xmlrpc_method(False)
356 def patch_get_by_hash(hash):
357 """Return structure for the given patch hash."""
359 patch = Patch.objects.filter(hash = hash)[0]
360 return patch_to_dict(patch)
364 @xmlrpc_method(False)
365 def patch_get_by_project_hash(project, hash):
366 """Return structure for the given patch hash."""
368 patch = Patch.objects.filter(project__linkname = project,
370 return patch_to_dict(patch)
374 @xmlrpc_method(False)
375 def patch_get_mbox(patch_id):
376 """Return mbox string for the given patch ID."""
378 patch = Patch.objects.filter(id = patch_id)[0]
379 return patch.mbox().as_string()
383 @xmlrpc_method(False)
384 def patch_get_diff(patch_id):
385 """Return diff for the given patch ID."""
387 patch = Patch.objects.filter(id = patch_id)[0]
393 def patch_set(user, patch_id, params):
394 """Update a patch with the key,value pairs in params. Only some parameters
397 ok_params = ['state', 'commit_ref', 'archived']
399 patch = Patch.objects.get(id = patch_id)
401 if not patch.is_editable(user):
402 raise Exception('No permissions to edit this patch')
404 for (k, v) in params.iteritems():
405 if k not in ok_params:
409 patch.state = State.objects.get(id = v)
421 @xmlrpc_method(False)
422 def state_list(search_str="", max_count=0):
423 """Get a list of state structures matching the given search string."""
425 if len(search_str) > 0:
426 states = State.objects.filter(name__icontains = search_str)
428 states = State.objects.all()
431 return map(state_to_dict, states)[:max_count]
433 return map(state_to_dict, states)
437 @xmlrpc_method(False)
438 def state_get(state_id):
439 """Return structure for the given state ID."""
441 state = State.objects.filter(id = state_id)[0]
442 return state_to_dict(state)