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