# Patchwork - automated patch tracking system # Copyright (C) 2008 Jeremy Kerr # # This file is part of the Patchwork package. # # Patchwork is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Patchwork is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Patchwork XMLRPC interface # from SimpleXMLRPCServer import SimpleXMLRPCDispatcher from django.http import HttpResponse, HttpResponseRedirect, \ HttpResponseServerError from django.core import urlresolvers from django.contrib.auth import authenticate from patchwork.models import Patch, Project, Person, State from patchwork.views import patch_to_mbox from django.views.decorators.csrf import csrf_exempt import sys import base64 import xmlrpclib class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher): def __init__(self): if sys.version_info[:3] >= (2,5,): SimpleXMLRPCDispatcher.__init__(self, allow_none=False, encoding=None) def _dumps(obj, *args, **kwargs): kwargs['allow_none'] = self.allow_none kwargs['encoding'] = self.encoding return xmlrpclib.dumps(obj, *args, **kwargs) else: def _dumps(obj, *args, **kwargs): return xmlrpclib.dumps(obj, *args, **kwargs) SimpleXMLRPCDispatcher.__init__(self) self.dumps = _dumps # map of name => (auth, func) self.func_map = {} def register_function(self, fn, auth_required): self.func_map[fn.__name__] = (auth_required, fn) def _user_for_request(self, request): auth_header = None if 'HTTP_AUTHORIZATION' in request.META: auth_header = request.META.get('HTTP_AUTHORIZATION') elif 'Authorization' in request.META: auth_header = request.META.get('Authorization') if auth_header is None or auth_header == '': raise Exception("No authentication credentials given") str = auth_header.strip() if not str.startswith('Basic '): raise Exception("Authentication scheme not supported") str = str[len('Basic '):].strip() try: decoded = base64.decodestring(str) username, password = decoded.split(':', 1) except: raise Exception("Invalid authentication credentials") return authenticate(username = username, password = password) def _dispatch(self, request, method, params): if method not in self.func_map.keys(): raise Exception('method "%s" is not supported' % method) auth_required, fn = self.func_map[method] if auth_required: user = self._user_for_request(request) if not user: raise Exception("Invalid username/password") params = (user,) + params return fn(*params) def _marshaled_dispatch(self, request): try: params, method = xmlrpclib.loads(request.body) response = self._dispatch(request, method, params) # wrap response in a singleton tuple response = (response,) response = self.dumps(response, methodresponse=1) except xmlrpclib.Fault, fault: response = self.dumps(fault) except: # report exception back to server response = self.dumps( xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)), ) return response dispatcher = PatchworkXMLRPCDispatcher() # XMLRPC view function @csrf_exempt def xmlrpc(request): if request.method != 'POST': return HttpResponseRedirect( urlresolvers.reverse('patchwork.views.help', kwargs = {'path': 'pwclient/'})) response = HttpResponse() try: ret = dispatcher._marshaled_dispatch(request) response.write(ret) except Exception: return HttpResponseServerError() return response # decorator for XMLRPC methods. Setting login_required to true will call # the decorated function with a non-optional user as the first argument. def xmlrpc_method(login_required = False): def wrap(f): dispatcher.register_function(f, login_required) return f return wrap # We allow most of the Django field lookup types for remote queries LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt", "in", "startswith", "istartswith", "endswith", "iendswith", "range", "year", "month", "day", "isnull" ] ####################################################################### # Helper functions ####################################################################### def project_to_dict(obj): """Return a trimmed down dictionary representation of a Project object which is OK to send to the client.""" return \ { 'id' : obj.id, 'linkname' : obj.linkname, 'name' : obj.name, } def person_to_dict(obj): """Return a trimmed down dictionary representation of a Person object which is OK to send to the client.""" # Make sure we don't return None even if the user submitted a patch # with no real name. XMLRPC can't marshall None. if obj.name is not None: name = obj.name else: name = obj.email return \ { 'id' : obj.id, 'email' : obj.email, 'name' : name, 'user' : unicode(obj.user).encode("utf-8"), } def patch_to_dict(obj): """Return a trimmed down dictionary representation of a Patch object which is OK to send to the client.""" return \ { 'id' : obj.id, 'date' : unicode(obj.date).encode("utf-8"), 'filename' : obj.filename(), 'msgid' : obj.msgid, 'name' : obj.name, 'project' : unicode(obj.project).encode("utf-8"), 'project_id' : obj.project_id, 'state' : unicode(obj.state).encode("utf-8"), 'state_id' : obj.state_id, 'submitter' : unicode(obj.submitter).encode("utf-8"), 'submitter_id' : obj.submitter_id, 'delegate' : unicode(obj.delegate).encode("utf-8"), 'delegate_id' : max(obj.delegate_id, 0), 'commit_ref' : max(obj.commit_ref, ''), } def bundle_to_dict(obj): """Return a trimmed down dictionary representation of a Bundle object which is OK to send to the client.""" return \ { 'id' : obj.id, 'name' : obj.name, 'n_patches' : obj.n_patches(), 'public_url' : obj.public_url(), } def state_to_dict(obj): """Return a trimmed down dictionary representation of a State object which is OK to send to the client.""" return \ { 'id' : obj.id, 'name' : obj.name, } ####################################################################### # Public XML-RPC methods ####################################################################### @xmlrpc_method(False) def pw_rpc_version(): """Return Patchwork XML-RPC interface version.""" return 1 @xmlrpc_method(False) def project_list(search_str="", max_count=0): """Get a list of projects matching the given filters.""" try: if len(search_str) > 0: projects = Project.objects.filter(linkname__icontains = search_str) else: projects = Project.objects.all() if max_count > 0: return map(project_to_dict, projects)[:max_count] else: return map(project_to_dict, projects) except: return [] @xmlrpc_method(False) def project_get(project_id): """Return structure for the given project ID.""" try: project = Project.objects.filter(id = project_id)[0] return project_to_dict(project) except: return {} @xmlrpc_method(False) def person_list(search_str="", max_count=0): """Get a list of Person objects matching the given filters.""" try: if len(search_str) > 0: people = (Person.objects.filter(name__icontains = search_str) | Person.objects.filter(email__icontains = search_str)) else: people = Person.objects.all() if max_count > 0: return map(person_to_dict, people)[:max_count] else: return map(person_to_dict, people) except: return [] @xmlrpc_method(False) def person_get(person_id): """Return structure for the given person ID.""" try: person = Person.objects.filter(id = person_id)[0] return person_to_dict(person) except: return {} @xmlrpc_method(False) def patch_list(filter={}): """Get a list of patches matching the given filters.""" try: # We allow access to many of the fields. But, some fields are # filtered by raw object so we must lookup by ID instead over # XML-RPC. ok_fields = [ "id", "name", "project_id", "submitter_id", "delegate_id", "state_id", "date", "commit_ref", "hash", "msgid", "max_count", ] dfilter = {} max_count = 0 for key in filter: parts = key.split("__") if parts[0] not in ok_fields: # Invalid field given return [] if len(parts) > 1: if LOOKUP_TYPES.count(parts[1]) == 0: # Invalid lookup type given return [] if parts[0] == 'project_id': dfilter['project'] = Project.objects.filter(id = filter[key])[0] elif parts[0] == 'submitter_id': dfilter['submitter'] = Person.objects.filter(id = filter[key])[0] elif parts[0] == 'delegate_id': dfilter['delegate'] = Person.objects.filter(id = filter[key])[0] elif parts[0] == 'state_id': dfilter['state'] = State.objects.filter(id = filter[key])[0] elif parts[0] == 'max_count': max_count = filter[key] else: dfilter[key] = filter[key] patches = Patch.objects.filter(**dfilter) if max_count > 0: return map(patch_to_dict, patches[:max_count]) else: return map(patch_to_dict, patches) except: return [] @xmlrpc_method(False) def patch_get(patch_id): """Return structure for the given patch ID.""" try: patch = Patch.objects.filter(id = patch_id)[0] return patch_to_dict(patch) except: return {} @xmlrpc_method(False) def patch_get_by_hash(hash): """Return structure for the given patch hash.""" try: patch = Patch.objects.filter(hash = hash)[0] return patch_to_dict(patch) except: return {} @xmlrpc_method(False) def patch_get_by_project_hash(project, hash): """Return structure for the given patch hash.""" try: patch = Patch.objects.filter(project__linkname = project, hash = hash)[0] return patch_to_dict(patch) except: return {} @xmlrpc_method(False) def patch_get_mbox(patch_id): """Return mbox string for the given patch ID.""" try: patch = Patch.objects.filter(id = patch_id)[0] return patch_to_mbox(patch).as_string() except: return "" @xmlrpc_method(False) def patch_get_diff(patch_id): """Return diff for the given patch ID.""" try: patch = Patch.objects.filter(id = patch_id)[0] return patch.content except: return "" @xmlrpc_method(True) def patch_set(user, patch_id, params): """Update a patch with the key,value pairs in params. Only some parameters can be set""" try: ok_params = ['state', 'commit_ref', 'archived'] patch = Patch.objects.get(id = patch_id) if not patch.is_editable(user): raise Exception('No permissions to edit this patch') for (k, v) in params.iteritems(): if k not in ok_params: continue if k == 'state': patch.state = State.objects.get(id = v) else: setattr(patch, k, v) patch.save() return True except: raise @xmlrpc_method(False) def state_list(search_str="", max_count=0): """Get a list of state structures matching the given search string.""" try: if len(search_str) > 0: states = State.objects.filter(name__icontains = search_str) else: states = State.objects.all() if max_count > 0: return map(state_to_dict, states)[:max_count] else: return map(state_to_dict, states) except: return [] @xmlrpc_method(False) def state_get(state_id): """Return structure for the given state ID.""" try: state = State.objects.filter(id = state_id)[0] return state_to_dict(state) except: return {}