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