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