]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/views/xmlrpc.py
tox: Add tox.ini file
[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 patchwork.views import patch_to_mbox
30 from django.views.decorators.csrf import csrf_exempt
31
32 import sys
33 import base64
34 import xmlrpclib
35
36 class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher):
37     def __init__(self):
38         if sys.version_info[:3] >= (2,5,):
39             SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
40                     encoding=None)
41             def _dumps(obj, *args, **kwargs):
42                 kwargs['allow_none'] = self.allow_none
43                 kwargs['encoding'] = self.encoding
44                 return xmlrpclib.dumps(obj, *args, **kwargs)
45         else:
46             def _dumps(obj, *args, **kwargs):
47                 return xmlrpclib.dumps(obj, *args, **kwargs)
48             SimpleXMLRPCDispatcher.__init__(self)
49
50         self.dumps = _dumps
51
52         # map of name => (auth, func)
53         self.func_map = {}
54
55     def register_function(self, fn, auth_required):
56         self.func_map[fn.__name__] = (auth_required, fn)
57
58
59     def _user_for_request(self, request):
60         auth_header = None
61
62         if 'HTTP_AUTHORIZATION' in request.META:
63             auth_header = request.META.get('HTTP_AUTHORIZATION')
64         elif 'Authorization' in request.META:
65             auth_header = request.META.get('Authorization')
66
67         if auth_header is None or auth_header == '':
68             raise Exception("No authentication credentials given")
69
70         str = auth_header.strip()
71
72         if not str.startswith('Basic '):
73             raise Exception("Authentication scheme not supported")
74
75         str = str[len('Basic '):].strip()
76
77         try:
78             decoded = base64.decodestring(str)
79             username, password = decoded.split(':', 1)
80         except:
81             raise Exception("Invalid authentication credentials")
82
83         return authenticate(username = username, password = password)
84
85
86     def _dispatch(self, request, method, params):
87         if method not in self.func_map.keys():
88             raise Exception('method "%s" is not supported' % method)
89
90         auth_required, fn = self.func_map[method]
91
92         if auth_required:
93             user = self._user_for_request(request)
94             if not user:
95                 raise Exception("Invalid username/password")
96
97             params = (user,) + params
98
99         return fn(*params)
100
101     def _marshaled_dispatch(self, request):
102         try:
103             params, method = xmlrpclib.loads(request.body)
104
105             response = self._dispatch(request, method, params)
106             # wrap response in a singleton tuple
107             response = (response,)
108             response = self.dumps(response, methodresponse=1)
109         except xmlrpclib.Fault, fault:
110             response = self.dumps(fault)
111         except:
112             # report exception back to server
113             response = self.dumps(
114                 xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)),
115                 )
116
117         return response
118
119 dispatcher = PatchworkXMLRPCDispatcher()
120
121 # XMLRPC view function
122 @csrf_exempt
123 def xmlrpc(request):
124     if request.method != 'POST':
125         return HttpResponseRedirect(
126                 urlresolvers.reverse('patchwork.views.help',
127                     kwargs = {'path': 'pwclient/'}))
128
129     response = HttpResponse()
130     try:
131         ret = dispatcher._marshaled_dispatch(request)
132         response.write(ret)
133     except Exception:
134         return HttpResponseServerError()
135
136     return response
137
138 # decorator for XMLRPC methods. Setting login_required to true will call
139 # the decorated function with a non-optional user as the first argument.
140 def xmlrpc_method(login_required = False):
141     def wrap(f):
142         dispatcher.register_function(f, login_required)
143         return f
144
145     return wrap
146
147
148
149 # We allow most of the Django field lookup types for remote queries
150 LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt",
151                 "in", "startswith", "istartswith", "endswith",
152                 "iendswith", "range", "year", "month", "day", "isnull" ]
153
154 #######################################################################
155 # Helper functions
156 #######################################################################
157
158 def project_to_dict(obj):
159     """Return a trimmed down dictionary representation of a Project
160     object which is OK to send to the client."""
161     return \
162         {
163          'id'           : obj.id,
164          'linkname'     : obj.linkname,
165          'name'         : obj.name,
166         }
167
168 def person_to_dict(obj):
169     """Return a trimmed down dictionary representation of a Person
170     object which is OK to send to the client."""
171
172     # Make sure we don't return None even if the user submitted a patch
173     # with no real name.  XMLRPC can't marshall None.
174     if obj.name is not None:
175         name = obj.name
176     else:
177         name = obj.email
178
179     return \
180         {
181          'id'           : obj.id,
182          'email'        : obj.email,
183          'name'         : name,
184          'user'         : unicode(obj.user).encode("utf-8"),
185         }
186
187 def patch_to_dict(obj):
188     """Return a trimmed down dictionary representation of a Patch
189     object which is OK to send to the client."""
190     return \
191         {
192          'id'           : obj.id,
193          'date'         : unicode(obj.date).encode("utf-8"),
194          'filename'     : obj.filename(),
195          'msgid'        : obj.msgid,
196          'name'         : obj.name,
197          'project'      : unicode(obj.project).encode("utf-8"),
198          'project_id'   : obj.project_id,
199          'state'        : unicode(obj.state).encode("utf-8"),
200          'state_id'     : obj.state_id,
201          'archived'     : obj.archived,
202          'submitter'    : unicode(obj.submitter).encode("utf-8"),
203          'submitter_id' : obj.submitter_id,
204          'delegate'     : unicode(obj.delegate).encode("utf-8"),
205          'delegate_id'  : max(obj.delegate_id, 0),
206          'commit_ref'   : max(obj.commit_ref, ''),
207         }
208
209 def bundle_to_dict(obj):
210     """Return a trimmed down dictionary representation of a Bundle
211     object which is OK to send to the client."""
212     return \
213         {
214          'id'           : obj.id,
215          'name'         : obj.name,
216          'n_patches'    : obj.n_patches(),
217          'public_url'   : obj.public_url(),
218         }
219
220 def state_to_dict(obj):
221     """Return a trimmed down dictionary representation of a State
222     object which is OK to send to the client."""
223     return \
224         {
225          'id'           : obj.id,
226          'name'         : obj.name,
227         }
228
229 #######################################################################
230 # Public XML-RPC methods
231 #######################################################################
232
233 @xmlrpc_method(False)
234 def pw_rpc_version():
235     """Return Patchwork XML-RPC interface version."""
236     return 1
237
238 @xmlrpc_method(False)
239 def project_list(search_str="", max_count=0):
240     """Get a list of projects matching the given filters."""
241     try:
242         if len(search_str) > 0:
243             projects = Project.objects.filter(linkname__icontains = search_str)
244         else:
245             projects = Project.objects.all()
246
247         if max_count > 0:
248             return map(project_to_dict, projects)[:max_count]
249         else:
250             return map(project_to_dict, projects)
251     except:
252         return []
253
254 @xmlrpc_method(False)
255 def project_get(project_id):
256     """Return structure for the given project ID."""
257     try:
258         project = Project.objects.filter(id = project_id)[0]
259         return project_to_dict(project)
260     except:
261         return {}
262
263 @xmlrpc_method(False)
264 def person_list(search_str="", max_count=0):
265     """Get a list of Person objects matching the given filters."""
266     try:
267         if len(search_str) > 0:
268             people = (Person.objects.filter(name__icontains = search_str) |
269                 Person.objects.filter(email__icontains = search_str))
270         else:
271             people = Person.objects.all()
272
273         if max_count > 0:
274             return map(person_to_dict, people)[:max_count]
275         else:
276             return map(person_to_dict, people)
277
278     except:
279         return []
280
281 @xmlrpc_method(False)
282 def person_get(person_id):
283     """Return structure for the given person ID."""
284     try:
285         person = Person.objects.filter(id = person_id)[0]
286         return person_to_dict(person)
287     except:
288         return {}
289
290 @xmlrpc_method(False)
291 def patch_list(filter={}):
292     """Get a list of patches matching the given filters."""
293     try:
294         # We allow access to many of the fields.  But, some fields are
295         # filtered by raw object so we must lookup by ID instead over
296         # XML-RPC.
297         ok_fields = [
298             "id",
299             "name",
300             "project_id",
301             "submitter_id",
302             "delegate_id",
303             "archived",
304             "state_id",
305             "date",
306             "commit_ref",
307             "hash",
308             "msgid",
309             "max_count",
310             ]
311
312         dfilter = {}
313         max_count = 0
314
315         for key in filter:
316             parts = key.split("__")
317             if parts[0] not in ok_fields:
318                 # Invalid field given
319                 return []
320             if len(parts) > 1:
321                 if LOOKUP_TYPES.count(parts[1]) == 0:
322                     # Invalid lookup type given
323                     return []
324
325             if parts[0] == 'project_id':
326                 dfilter['project'] = Project.objects.filter(id =
327                                         filter[key])[0]
328             elif parts[0] == 'submitter_id':
329                 dfilter['submitter'] = Person.objects.filter(id =
330                                         filter[key])[0]
331             elif parts[0] == 'delegate_id':
332                 dfilter['delegate'] = Person.objects.filter(id =
333                                         filter[key])[0]
334             elif parts[0] == 'state_id':
335                 dfilter['state'] = State.objects.filter(id =
336                                         filter[key])[0]
337             elif parts[0] == 'max_count':
338                 max_count = filter[key]
339             else:
340                 dfilter[key] = filter[key]
341
342         patches = Patch.objects.filter(**dfilter)
343
344         if max_count > 0:
345             return map(patch_to_dict, patches[:max_count])
346         else:
347             return map(patch_to_dict, patches)
348
349     except:
350         return []
351
352 @xmlrpc_method(False)
353 def patch_get(patch_id):
354     """Return structure for the given patch ID."""
355     try:
356         patch = Patch.objects.filter(id = patch_id)[0]
357         return patch_to_dict(patch)
358     except:
359         return {}
360
361 @xmlrpc_method(False)
362 def patch_get_by_hash(hash):
363     """Return structure for the given patch hash."""
364     try:
365         patch = Patch.objects.filter(hash = hash)[0]
366         return patch_to_dict(patch)
367     except:
368         return {}
369
370 @xmlrpc_method(False)
371 def patch_get_by_project_hash(project, hash):
372     """Return structure for the given patch hash."""
373     try:
374         patch = Patch.objects.filter(project__linkname = project,
375                                      hash = hash)[0]
376         return patch_to_dict(patch)
377     except:
378         return {}
379
380 @xmlrpc_method(False)
381 def patch_get_mbox(patch_id):
382     """Return mbox string for the given patch ID."""
383     try:
384         patch = Patch.objects.filter(id = patch_id)[0]
385         return patch_to_mbox(patch).as_string(True)
386     except:
387         return ""
388
389 @xmlrpc_method(False)
390 def patch_get_diff(patch_id):
391     """Return diff for the given patch ID."""
392     try:
393         patch = Patch.objects.filter(id = patch_id)[0]
394         return patch.content
395     except:
396         return ""
397
398 @xmlrpc_method(True)
399 def patch_set(user, patch_id, params):
400     """Update a patch with the key,value pairs in params. Only some parameters
401        can be set"""
402     try:
403         ok_params = ['state', 'commit_ref', 'archived']
404
405         patch = Patch.objects.get(id = patch_id)
406
407         if not patch.is_editable(user):
408             raise Exception('No permissions to edit this patch')
409
410         for (k, v) in params.iteritems():
411             if k not in ok_params:
412                 continue
413
414             if k == 'state':
415                 patch.state = State.objects.get(id = v)
416
417             else:
418                 setattr(patch, k, v)
419
420         patch.save()
421
422         return True
423
424     except:
425         raise
426
427 @xmlrpc_method(False)
428 def state_list(search_str="", max_count=0):
429     """Get a list of state structures matching the given search string."""
430     try:
431         if len(search_str) > 0:
432             states = State.objects.filter(name__icontains = search_str)
433         else:
434             states = State.objects.all()
435
436         if max_count > 0:
437             return map(state_to_dict, states)[:max_count]
438         else:
439             return map(state_to_dict, states)
440     except:
441         return []
442
443 @xmlrpc_method(False)
444 def state_get(state_id):
445     """Return structure for the given state ID."""
446     try:
447         state = State.objects.filter(id = state_id)[0]
448         return state_to_dict(state)
449     except:
450         return {}