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