]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/views/xmlrpc.py
a69c8586b985a764623d71a4db33bdb82c1b2ac7
[patchwork] / apps / patchwork / views / xmlrpc.py
1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
3 #
4 # This file is part of the Patchwork package.
5 #
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.
10 #
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.
15 #
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
19 #
20 # Patchwork XMLRPC interface
21 #
22
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
30
31 import sys
32 import base64
33 import xmlrpclib
34
35 class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher):
36     def __init__(self):
37         if sys.version_info[:3] >= (2,5,):
38             SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
39                     encoding=None)
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)
44         else:
45             def _dumps(obj, *args, **kwargs):
46                 return xmlrpclib.dumps(obj, *args, **kwargs)
47             SimpleXMLRPCDispatcher.__init__(self)
48
49         self.dumps = _dumps
50
51         # map of name => (auth, func)
52         self.func_map = {}
53
54     def register_function(self, fn, auth_required):
55         self.func_map[fn.__name__] = (auth_required, fn)
56
57
58     def _user_for_request(self, request):
59         auth_header = None
60
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')
65
66         if auth_header is None or auth_header == '':
67             raise Exception("No authentication credentials given")
68
69         str = auth_header.strip()
70
71         if not str.startswith('Basic '):
72             raise Exception("Authentication scheme not supported")
73
74         str = str[len('Basic '):].strip()
75
76         try:
77             decoded = base64.decodestring(str)
78             username, password = decoded.split(':', 1)
79         except:
80             raise Exception("Invalid authentication credentials")
81
82         return authenticate(username = username, password = password)
83
84
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)
88
89         auth_required, fn = self.func_map[method]
90
91         if auth_required:
92             user = self._user_for_request(request)
93             if not user:
94                 raise Exception("Invalid username/password")
95
96             params = (user,) + params
97
98         return fn(*params)
99
100     def _marshaled_dispatch(self, request):
101         try:
102             params, method = xmlrpclib.loads(request.raw_post_data)
103
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)
110         except:
111             # report exception back to server
112             response = self.dumps(
113                 xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)),
114                 )
115
116         return response
117
118 dispatcher = PatchworkXMLRPCDispatcher()
119
120 # XMLRPC view function
121 @csrf_exempt
122 def xmlrpc(request):
123     if request.method != 'POST':
124         return HttpResponseRedirect(
125                 urlresolvers.reverse('patchwork.views.help',
126                     kwargs = {'path': 'pwclient/'}))
127
128     response = HttpResponse()
129     try:
130         ret = dispatcher._marshaled_dispatch(request)
131         response.write(ret)
132     except Exception:
133         return HttpResponseServerError()
134
135     return response
136
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):
140     def wrap(f):
141         dispatcher.register_function(f, login_required)
142         return f
143
144     return wrap
145
146
147
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" ]
152
153 #######################################################################
154 # Helper functions
155 #######################################################################
156
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."""
160     return \
161         {
162          'id'           : obj.id,
163          'linkname'     : obj.linkname,
164          'name'         : obj.name,
165         }
166
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."""
170
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:
174         name = obj.name
175     else:
176         name = obj.email
177
178     return \
179         {
180          'id'           : obj.id,
181          'email'        : obj.email,
182          'name'         : name,
183          'user'         : unicode(obj.user).encode("utf-8"),
184         }
185
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."""
189     return \
190         {
191          'id'           : obj.id,
192          'date'         : unicode(obj.date).encode("utf-8"),
193          'filename'     : obj.filename(),
194          'msgid'        : obj.msgid,
195          'name'         : obj.name,
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, ''),
205         }
206
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."""
210     return \
211         {
212          'id'           : obj.id,
213          'name'         : obj.name,
214          'n_patches'    : obj.n_patches(),
215          'public_url'   : obj.public_url(),
216         }
217
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."""
221     return \
222         {
223          'id'           : obj.id,
224          'name'         : obj.name,
225         }
226
227 #######################################################################
228 # Public XML-RPC methods
229 #######################################################################
230
231 @xmlrpc_method(False)
232 def pw_rpc_version():
233     """Return Patchwork XML-RPC interface version."""
234     return 1
235
236 @xmlrpc_method(False)
237 def project_list(search_str="", max_count=0):
238     """Get a list of projects matching the given filters."""
239     try:
240         if len(search_str) > 0:
241             projects = Project.objects.filter(linkname__icontains = search_str)
242         else:
243             projects = Project.objects.all()
244
245         if max_count > 0:
246             return map(project_to_dict, projects)[:max_count]
247         else:
248             return map(project_to_dict, projects)
249     except:
250         return []
251
252 @xmlrpc_method(False)
253 def project_get(project_id):
254     """Return structure for the given project ID."""
255     try:
256         project = Project.objects.filter(id = project_id)[0]
257         return project_to_dict(project)
258     except:
259         return {}
260
261 @xmlrpc_method(False)
262 def person_list(search_str="", max_count=0):
263     """Get a list of Person objects matching the given filters."""
264     try:
265         if len(search_str) > 0:
266             people = (Person.objects.filter(name__icontains = search_str) |
267                 Person.objects.filter(email__icontains = search_str))
268         else:
269             people = Person.objects.all()
270
271         if max_count > 0:
272             return map(person_to_dict, people)[:max_count]
273         else:
274             return map(person_to_dict, people)
275
276     except:
277         return []
278
279 @xmlrpc_method(False)
280 def person_get(person_id):
281     """Return structure for the given person ID."""
282     try:
283         person = Person.objects.filter(id = person_id)[0]
284         return person_to_dict(person)
285     except:
286         return {}
287
288 @xmlrpc_method(False)
289 def patch_list(filter={}):
290     """Get a list of patches matching the given filters."""
291     try:
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
294         # XML-RPC.
295         ok_fields = [
296             "id",
297             "name",
298             "project_id",
299             "submitter_id",
300             "delegate_id",
301             "state_id",
302             "date",
303             "commit_ref",
304             "hash",
305             "msgid",
306             "max_count",
307             ]
308
309         dfilter = {}
310         max_count = 0
311
312         for key in filter:
313             parts = key.split("__")
314             if parts[0] not in ok_fields:
315                 # Invalid field given
316                 return []
317             if len(parts) > 1:
318                 if LOOKUP_TYPES.count(parts[1]) == 0:
319                     # Invalid lookup type given
320                     return []
321
322             if parts[0] == 'project_id':
323                 dfilter['project'] = Project.objects.filter(id =
324                                         filter[key])[0]
325             elif parts[0] == 'submitter_id':
326                 dfilter['submitter'] = Person.objects.filter(id =
327                                         filter[key])[0]
328             elif parts[0] == 'state_id':
329                 dfilter['state'] = State.objects.filter(id =
330                                         filter[key])[0]
331             elif parts[0] == 'max_count':
332                 max_count = filter[key]
333             else:
334                 dfilter[key] = filter[key]
335
336         patches = Patch.objects.filter(**dfilter)
337
338         if max_count > 0:
339             return map(patch_to_dict, patches[:max_count])
340         else:
341             return map(patch_to_dict, patches)
342
343     except:
344         return []
345
346 @xmlrpc_method(False)
347 def patch_get(patch_id):
348     """Return structure for the given patch ID."""
349     try:
350         patch = Patch.objects.filter(id = patch_id)[0]
351         return patch_to_dict(patch)
352     except:
353         return {}
354
355 @xmlrpc_method(False)
356 def patch_get_by_hash(hash):
357     """Return structure for the given patch hash."""
358     try:
359         patch = Patch.objects.filter(hash = hash)[0]
360         return patch_to_dict(patch)
361     except:
362         return {}
363
364 @xmlrpc_method(False)
365 def patch_get_by_project_hash(project, hash):
366     """Return structure for the given patch hash."""
367     try:
368         patch = Patch.objects.filter(project__linkname = project,
369                                      hash = hash)[0]
370         return patch_to_dict(patch)
371     except:
372         return {}
373
374 @xmlrpc_method(False)
375 def patch_get_mbox(patch_id):
376     """Return mbox string for the given patch ID."""
377     try:
378         patch = Patch.objects.filter(id = patch_id)[0]
379         return patch.mbox().as_string()
380     except:
381         return ""
382
383 @xmlrpc_method(False)
384 def patch_get_diff(patch_id):
385     """Return diff for the given patch ID."""
386     try:
387         patch = Patch.objects.filter(id = patch_id)[0]
388         return patch.content
389     except:
390         return ""
391
392 @xmlrpc_method(True)
393 def patch_set(user, patch_id, params):
394     """Update a patch with the key,value pairs in params. Only some parameters
395        can be set"""
396     try:
397         ok_params = ['state', 'commit_ref', 'archived']
398
399         patch = Patch.objects.get(id = patch_id)
400
401         if not patch.is_editable(user):
402             raise Exception('No permissions to edit this patch')
403
404         for (k, v) in params.iteritems():
405             if k not in ok_params:
406                 continue
407
408             if k == 'state':
409                 patch.state = State.objects.get(id = v)
410
411             else:
412                 setattr(patch, k, v)
413
414         patch.save()
415
416         return True
417
418     except:
419         raise
420
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."""
424     try:
425         if len(search_str) > 0:
426             states = State.objects.filter(name__icontains = search_str)
427         else:
428             states = State.objects.all()
429
430         if max_count > 0:
431             return map(state_to_dict, states)[:max_count]
432         else:
433             return map(state_to_dict, states)
434     except:
435         return []
436
437 @xmlrpc_method(False)
438 def state_get(state_id):
439     """Return structure for the given state ID."""
440     try:
441         state = State.objects.filter(id = state_id)[0]
442         return state_to_dict(state)
443     except:
444         return {}