xmlrpcplugin/0000755000175000017500000000000012745672763011761 5ustar wmbwmbxmlrpcplugin/0.10/0000755000175000017500000000000010601605463012315 5ustar wmbwmbxmlrpcplugin/0.10/tracrpc/0000755000175000017500000000000010563613640013756 5ustar wmbwmbxmlrpcplugin/0.10/tracrpc/wiki.py0000644000175000017500000001621210543501252015266 0ustar wmbwmbtry: from cStringIO import StringIO except ImportError: from StringIO import StringIO import xmlrpclib import posixpath from trac.core import * from trac.perm import IPermissionRequestor from trac.wiki.api import WikiSystem from trac.wiki.model import WikiPage from trac.wiki.formatter import wiki_to_html from trac.attachment import Attachment from tracrpc.api import IXMLRPCHandler, expose_rpc from tracrpc.util import to_timestamp class WikiRPC(Component): """ Implementation of the [http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2 WikiRPC API]. """ implements(IXMLRPCHandler) def __init__(self): self.wiki = WikiSystem(self.env) def xmlrpc_namespace(self): return 'wiki' def xmlrpc_methods(self): yield ('WIKI_VIEW', ((dict, xmlrpclib.DateTime),), self.getRecentChanges) yield ('WIKI_VIEW', ((int,),), self.getRPCVersionSupported) yield ('WIKI_VIEW', ((str, str), (str, str, int),), self.getPage) yield ('WIKI_VIEW', ((str, str, int),), self.getPage, 'getPageVersion') yield ('WIKI_VIEW', ((str, str), (str, str, int)), self.getPageHTML) yield ('WIKI_VIEW', ((str, str), (str, str, int)), self.getPageHTML, 'getPageHTMLVersion') yield ('WIKI_VIEW', ((list,),), self.getAllPages) yield ('WIKI_VIEW', ((dict, str), (dict, str, int)), self.getPageInfo) yield ('WIKI_VIEW', ((dict, str, int),), self.getPageInfo, 'getPageInfoVersion') yield ('WIKI_CREATE', ((bool, str, str, dict),), self.putPage) yield ('WIKI_VIEW', ((list, str),), self.listAttachments) yield ('WIKI_VIEW', ((xmlrpclib.Binary, str),), self.getAttachment) yield ('WIKI_MODIFY', ((bool, str, xmlrpclib.Binary),), self.putAttachment) yield ('WIKI_MODIFY', ((bool, str, str, str, xmlrpclib.Binary), (bool, str, str, str, xmlrpclib.Binary, bool)), self.putAttachmentEx) yield ('WIKI_DELETE', ((bool, str),), self.deleteAttachment) yield ('WIKI_VIEW', ((list, str),), self.listLinks) yield ('WIKI_VIEW', ((str, str),), self.wikiToHtml) def _page_info(self, name, time, author, version): return dict(name=name, lastModified=xmlrpclib.DateTime(int(time)), author=author, version=int(version)) def getRecentChanges(self, req, since): """ Get list of changed pages since timestamp """ since = to_timestamp(since) db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute('SELECT name, max(time), author, version FROM wiki' ' WHERE time >= %s GROUP BY name ORDER BY max(time) DESC', (since,)) result = [] for name, time, author, version in cursor: result.append(self._page_info(name, time, author, version)) return result def getRPCVersionSupported(self, req): """ Returns 2 with this version of the Trac API. """ return 2 def getPage(self, req, pagename, version=None): """ Get the raw Wiki text of page, latest version. """ page = WikiPage(self.env, pagename, version) if page.exists: return page.text else: msg = 'Wiki page "%s" does not exist' % pagename if version is not None: msg += ' at version %s' % version raise xmlrpclib.Fault(0, msg) def getPageHTML(self, req, pagename, version=None): """ Return page in rendered HTML, latest version. """ text = self.getPage(req, pagename, version) html = wiki_to_html(text, self.env, req, absurls=1) return '%s' % html def getAllPages(self, req): """ Returns a list of all pages. The result is an array of utf8 pagenames. """ return list(self.wiki.get_pages()) def getPageInfo(self, req, pagename, version=None): """ Returns information about the given page. """ page = WikiPage(self.env, pagename, version) if page.exists: last_update = page.get_history().next() return self._page_info(page.name, last_update[1], last_update[2], page.version) def putPage(self, req, pagename, content, attributes): """ writes the content of the page. """ page = WikiPage(self.env, pagename) if page.readonly: req.perm.assert_permission('WIKI_ADMIN') elif not page.exists: req.perm.assert_permission('WIKI_CREATE') else: req.perm.assert_permission('WIKI_MODIFY') page.text = content if req.perm.has_permission('WIKI_ADMIN'): page.readonly = attributes.get('readonly') and 1 or 0 page.save(attributes.get('author', req.authname), attributes.get('comment'), req.remote_addr) return True def listAttachments(self, req, pagename): """ Lists attachments on a given page. """ return [pagename + '/' + a.filename for a in Attachment.select(self.env, 'wiki', pagename)] def getAttachment(self, req, path): """ returns the content of an attachment. """ pagename, filename = posixpath.split(path) attachment = Attachment(self.env, 'wiki', pagename, filename) return xmlrpclib.Binary(attachment.open().read()) def putAttachment(self, req, path, data): """ (over)writes an attachment. Returns True if successful. This method is compatible with WikiRPC. `putAttachmentEx` has a more extensive set of (Trac-specific) features. """ pagename, filename = posixpath.split(path) self.putAttachmentEx(req, pagename, filename, None, data) return True def putAttachmentEx(self, req, pagename, filename, description, data, replace=True): """ Attach a file to a Wiki page. Returns the (possibly transformed) filename of the attachment. Use this method if you don't care about WikiRPC compatibility. """ if not WikiPage(self.env, pagename).exists: raise TracError, 'Wiki page "%s" does not exist' % pagename if replace: try: attachment = Attachment(self.env, 'wiki', pagename, filename) attachment.delete() except TracError: pass attachment = Attachment(self.env, 'wiki', pagename) attachment.author = req.authname or 'anonymous' attachment.description = description attachment.insert(filename, StringIO(data.data), len(data.data)) return attachment.filename def deleteAttachment(self, req, path): """ Delete an attachment. """ pagename, filename = posixpath.split(path) if not WikiPage(self.env, pagename).exists: raise TracError, 'Wiki page "%s" does not exist' % pagename attachment = Attachment(self.env, 'wiki', pagename, filename) attachment.delete() return True def listLinks(self, req, pagename): """ ''Not implemented'' """ return [] def wikiToHtml(self, req, text): """ Render arbitrary Wiki text as HTML. """ return unicode(wiki_to_html(text, self.env, req, absurls=1)) xmlrpcplugin/0.10/tracrpc/web_ui.py0000644000175000017500000000626310472454454015616 0ustar wmbwmbfrom trac.core import * from trac.web.main import IRequestHandler from trac.web.chrome import ITemplateProvider, add_stylesheet from tracrpc.api import IXMLRPCHandler, XMLRPCSystem from trac.wiki.formatter import wiki_to_oneliner import xmlrpclib class XMLRPCWeb(Component): """ Handle XML-RPC calls from HTTP clients, as well as presenting a list of methods available to the currently logged in user. Browsing to /xmlrpc will display this list. """ implements(IRequestHandler, ITemplateProvider) # IRequestHandler methods def match_request(self, req): return req.path_info in ('/login/xmlrpc', '/xmlrpc') def _send_response(self, req, response): req.send_response(200) req.send_header('Content-Type', 'text/xml') req.send_header('Content-Length', len(response)) req.end_headers() req.write(response) def process_request(self, req): # Need at least XML_RPC req.perm.assert_permission('XML_RPC') # Dump RPC functions content_type = req.get_header('Content-Type') if content_type is None or 'text/xml' not in content_type: namespaces = {} for method in XMLRPCSystem(self.env).all_methods(req): namespace = method.namespace.replace('.', '_') if namespace not in namespaces: namespaces[namespace] = { 'description' : wiki_to_oneliner(method.namespace_description, self.env), 'methods' : [], 'namespace' : method.namespace, } try: namespaces[namespace]['methods'].append((method.signature, wiki_to_oneliner(method.description, self.env), method.permission)) except Exception, e: from StringIO import StringIO import traceback out = StringIO() traceback.print_exc(file=out) raise Exception('%s: %s\n%s' % (method.name, str(e), out.getvalue())) add_stylesheet(req, 'common/css/wiki.css') req.hdf['xmlrpc.functions'] = namespaces return 'xmlrpclist.cs', None # Handle XML-RPC call args, method = xmlrpclib.loads(req.read(int(req.get_header('Content-Length')))) try: result = XMLRPCSystem(self.env).get_method(method)(req, args) self._send_response(req, xmlrpclib.dumps(result, methodresponse=True)) except xmlrpclib.Fault, e: self.log.error(e) self._send_response(req, xmlrpclib.dumps(e)) except Exception, e: self.log.error(e) import traceback from StringIO import StringIO out = StringIO() traceback.print_exc(file = out) self.log.error(out.getvalue()) self._send_response(req, xmlrpclib.dumps(xmlrpclib.Fault(2, "'%s' while executing '%s()'" % (str(e), method)))) # ITemplateProvider def get_htdocs_dirs(self): return [] def get_templates_dirs(self): from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] xmlrpcplugin/0.10/tracrpc/templates/0000755000175000017500000000000010525202651015746 5ustar wmbwmbxmlrpcplugin/0.10/tracrpc/templates/xmlrpclist.cs0000644000175000017500000000175110525202651020502 0ustar wmbwmb

XML-RPC exported functions

> -

Function Description Permission required
xmlrpcplugin/0.10/tracrpc/__init__.py0000644000175000017500000000021410356104531016056 0ustar wmbwmbfrom tracrpc.api import * from tracrpc.web_ui import * from tracrpc.ticket import * from tracrpc.wiki import * from tracrpc.search import * xmlrpcplugin/0.10/tracrpc/util.py0000644000175000017500000000034010543501252015273 0ustar wmbwmbimport time def to_timestamp(datetime): """ Convert xmlrpclib.DateTime string representation to UNIX timestamp. """ return time.mktime(time.strptime('%s UTC' % datetime.value, '%Y%m%dT%H:%M:%S %Z')) - time.timezone xmlrpcplugin/0.10/tracrpc/search.py0000644000175000017500000000362010503746773015606 0ustar wmbwmbfrom trac.core import * from tracrpc.api import IXMLRPCHandler from trac.Search import ISearchSource try: a = set() except: from sets import Set as set class SearchRPC(Component): """ Search Trac. """ implements(IXMLRPCHandler) search_sources = ExtensionPoint(ISearchSource) # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'search' def xmlrpc_methods(self): yield ('SEARCH_VIEW', ((list,),), self.getSearchFilters) yield ('SEARCH_VIEW', ((list, str), (list, str, list)), self.performSearch) # Others def getSearchFilters(self, req): """ Retrieve a list of search filters with each element in the form (name, description). """ for source in self.search_sources: for filter in source.get_search_filters(req): yield filter def performSearch(self, req, query, filters = []): """ Perform a search using the given filters. Defaults to all if not provided. Results are returned as a list of tuples in the form (href, title, date, author, excerpt).""" from trac.Search import search_terms query = search_terms(query) chosen_filters = set(filters) available_filters = [] for source in self.search_sources: available_filters += source.get_search_filters(req) filters = [f[0] for f in available_filters if f[0] in chosen_filters] if not filters: filters = [f[0] for f in available_filters] self.env.log.debug("Searching with %s" % filters) results = [] for source in self.search_sources: for result in source.get_search_results(req, query, filters): result = map(unicode, result) results.append(['/'.join(req.base_url.split('/')[0:3]) + result[0]] + list(result[1:])) return results xmlrpcplugin/0.10/tracrpc/ticket.py0000644000175000017500000002577710563613640015635 0ustar wmbwmbfrom trac.attachment import Attachment from trac.core import * from tracrpc.api import IXMLRPCHandler, expose_rpc from tracrpc.util import to_timestamp import trac.ticket.model as model import trac.ticket.query as query from trac.ticket.api import TicketSystem from trac.ticket.notification import TicketNotifyEmail import time import pydoc import xmlrpclib from StringIO import StringIO class TicketRPC(Component): """ An interface to Trac's ticketing system. """ implements(IXMLRPCHandler) # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'ticket' def xmlrpc_methods(self): yield ('TICKET_VIEW', ((list,), (list, str)), self.query) yield ('TICKET_VIEW', ((list, xmlrpclib.DateTime),), self.getRecentChanges) yield ('TICKET_VIEW', ((list, int),), self.getAvailableActions) yield ('TICKET_VIEW', ((list, int),), self.get) yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool)), self.create) yield ('TICKET_APPEND', ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update) yield ('TICKET_ADMIN', ((None, int),), self.delete) yield ('TICKET_VIEW', ((dict, int), (dict, int, int)), self.changeLog) yield ('TICKET_VIEW', ((list, int),), self.listAttachments) yield ('TICKET_VIEW', ((xmlrpclib.Binary, int, str),), self.getAttachment) yield ('TICKET_APPEND', ((str, int, str, str, xmlrpclib.Binary, bool), (str, int, str, str, xmlrpclib.Binary)), self.putAttachment) yield ('TICKET_ADMIN', ((bool, int, str),), self.deleteAttachment) yield ('TICKET_VIEW', ((list,),), self.getTicketFields) # Exported methods def query(self, req, qstr='status!=closed'): """ Perform a ticket query, returning a list of ticket ID's. """ q = query.Query.from_string(self.env, qstr) out = [] for t in q.execute(req): out.append(t['id']) return out def getRecentChanges(self, req, since): """Returns a list of IDs of tickets that have changed since timestamp.""" since = to_timestamp(since) db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute('SELECT id FROM ticket' ' WHERE changetime >= %s', (since,)) result = [] for row in cursor: result.append(int(row[0])) return result def getAvailableActions(self, req, id): """Returns the actions that can be performed on the ticket.""" ticketSystem = TicketSystem(self.env) t = model.Ticket(self.env, id) return ticketSystem.get_available_actions(t, req.perm) def get(self, req, id): """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ t = model.Ticket(self.env, id) return (t.id, t.time_created, t.time_changed, t.values) def create(self, req, summary, description, attributes = {}, notify=False): """ Create a new ticket, returning the ticket ID. """ t = model.Ticket(self.env) t['status'] = 'new' t['summary'] = summary t['description'] = description t['reporter'] = req.authname or 'anonymous' for k, v in attributes.iteritems(): t[k] = v t.insert() if notify: try: tn = TicketNotifyEmail(self.env) tn.notify(t, newticket=True) except Exception, e: self.log.exception("Failure sending notification on creation " "of ticket #%s: %s" % (t.id, e)) return t.id def update(self, req, id, comment, attributes = {}, notify=False): """ Update a ticket, returning the new ticket in the same form as getTicket(). """ now = int(time.time()) t = model.Ticket(self.env, id) for k, v in attributes.iteritems(): t[k] = v t.save_changes(req.authname or 'anonymous', comment) if notify: try: tn = TicketNotifyEmail(self.env) tn.notify(t, newticket=False, modtime=now) except Exception, e: self.log.exception("Failure sending notification on change of " "ticket #%s: %s" % (t.id, e)) return self.get(req, t.id) def delete(self, req, id): """ Delete ticket with the given id. """ t = model.Ticket(self.env, id) t.delete() def changeLog(self, req, id, when=0): t = model.Ticket(self.env, id) return t.get_changelog(when) # Use existing documentation from Ticket model changeLog.__doc__ = pydoc.getdoc(model.Ticket.get_changelog) def listAttachments(self, req, ticket): """ Lists attachments for a given ticket. Returns (filename, description, size, time, author) for each attachment.""" for t in Attachment.select(self.env, 'ticket', ticket): yield (t.filename, t.description or '', t.size, t.time, t.author) def getAttachment(self, req, ticket, filename): """ returns the content of an attachment. """ attachment = Attachment(self.env, 'ticket', ticket, filename) return xmlrpclib.Binary(attachment.open().read()) def putAttachment(self, req, ticket, filename, description, data, replace=True): """ Add an attachment, optionally (and defaulting to) overwriting an existing one. Returns filename.""" if not model.Ticket(self.env, ticket).exists: raise TracError, 'Ticket "%s" does not exist' % ticket if replace: try: attachment = Attachment(self.env, 'ticket', ticket, filename) attachment.delete() except TracError: pass attachment = Attachment(self.env, 'ticket', ticket) attachment.author = req.authname or 'anonymous' attachment.description = description attachment.insert(filename, StringIO(data.data), len(data.data)) return attachment.filename def deleteAttachment(self, req, ticket, filename): """ Delete an attachment. """ if not model.Ticket(self.env, ticket).exists: raise TracError('Ticket "%s" does not exists' % ticket) attachment = Attachment(self.env, 'ticket', ticket, filename) attachment.delete() return True def getTicketFields(self, req): """ Return a list of all ticket fields fields. """ return TicketSystem(self.env).get_ticket_fields() def ticketModelFactory(cls, cls_attributes): """ Return a class which exports an interface to trac.ticket.model.. """ class TicketModelImpl(Component): implements(IXMLRPCHandler) def xmlrpc_namespace(self): return 'ticket.' + cls.__name__.lower() def xmlrpc_methods(self): yield ('TICKET_VIEW', ((list,),), self.getAll) yield ('TICKET_VIEW', ((dict, str),), self.get) yield ('TICKET_ADMIN', ((None, str,),), self.delete) yield ('TICKET_ADMIN', ((None, str, dict),), self.create) yield ('TICKET_ADMIN', ((None, str, dict),), self.update) def getAll(self, req): for i in cls.select(self.env): yield i.name getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() def get(self, req, name): i = cls(self.env, name) attributes= {} for k, default in cls_attributes.iteritems(): v = getattr(i, k) if v is None: v = default attributes[k] = v return attributes get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() def delete(self, req, name): cls(self.env, name).delete() delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() def create(self, req, name, attributes): i = cls(self.env) i.name = name for k, v in attributes.iteritems(): setattr(i, k, v) i.insert(); create.__doc__ = """ Create a new ticket %s with the given attributes. """ % cls.__name__.lower() def update(self, req, name, attributes): self._updateHelper(name, attributes).update() update.__doc__ = """ Update ticket %s with the given attributes. """ % cls.__name__.lower() def _updateHelper(self, name, attributes): i = cls(self.env, name) for k, v in attributes.iteritems(): setattr(i, k, v) return i TicketModelImpl.__doc__ = """ Interface to ticket %s objects. """ % cls.__name__.lower() TicketModelImpl.__name__ = '%sRPC' % cls.__name__ return TicketModelImpl def ticketEnumFactory(cls): """ Return a class which exports an interface to one of the Trac ticket abstract enum types. """ class AbstractEnumImpl(Component): implements(IXMLRPCHandler) def xmlrpc_namespace(self): return 'ticket.' + cls.__name__.lower() def xmlrpc_methods(self): yield ('TICKET_VIEW', ((list,),), self.getAll) yield ('TICKET_VIEW', ((str, str),), self.get) yield ('TICKET_ADMIN', ((None, str,),), self.delete) yield ('TICKET_ADMIN', ((None, str, str),), self.create) yield ('TICKET_ADMIN', ((None, str, str),), self.update) def getAll(self, req): for i in cls.select(self.env): yield i.name getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() def get(self, req, name): i = cls(self.env, name) return i.value get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() def delete(self, req, name): cls(self.env, name).delete() delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() def create(self, req, name, value): i = cls(self.env) i.name = name i.value = value i.insert() create.__doc__ = """ Create a new ticket %s with the given value. """ % cls.__name__.lower() def update(self, req, name, value): self._updateHelper(name, value).update() update.__doc__ = """ Update ticket %s with the given value. """ % cls.__name__.lower() def _updateHelper(self, name, value): i = cls(self.env, name) i.value = value return i AbstractEnumImpl.__doc__ = """ Interface to ticket %s. """ % cls.__name__.lower() AbstractEnumImpl.__name__ = '%sRPC' % cls.__name__ return AbstractEnumImpl ticketModelFactory(model.Component, {'name': '', 'owner': '', 'description': ''}) ticketModelFactory(model.Version, {'name': '', 'time': 0, 'description': ''}) ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''}) ticketEnumFactory(model.Type) ticketEnumFactory(model.Status) ticketEnumFactory(model.Resolution) ticketEnumFactory(model.Priority) ticketEnumFactory(model.Severity) xmlrpcplugin/0.10/tracrpc/api.py0000644000175000017500000002075010563613640015105 0ustar wmbwmbfrom trac.core import * from trac.perm import IPermissionRequestor import inspect import types import xmlrpclib try: set = set except: from sets import Set as set RPC_TYPES = {int: 'int', bool: 'boolean', str: 'string', float: 'double', xmlrpclib.DateTime: 'dateTime.iso8601', xmlrpclib.Binary: 'base64', list: 'array', dict: 'struct', None : 'int'} def expose_rpc(permission, return_type, *arg_types): """ Decorator for exposing a method as an RPC call with the given signature. """ def decorator(func): if not hasattr(func, '_xmlrpc_signatures'): func._xmlrpc_signatures = [] func._xml_rpc_permission = permission func._xmlrpc_signatures.append((return_type,) + tuple(arg_types)) return func return decorator class IXMLRPCHandler(Interface): def xmlrpc_namespace(): """ Provide the namespace in which a set of methods lives. This can be overridden if the 'name' element is provided by xmlrpc_methods(). """ def xmlrpc_methods(): """ Return an iterator of (permission, signatures, callable[, name]), where callable is exposed via XML-RPC if the authenticated user has the appropriate permission. The callable itself can be a method or a normal method. The first argument passed will always be a request object. The XMLRPCSystem performs some extra magic to remove the "self" and "req" arguments when listing the available methods. Signatures is a list of XML-RPC introspection signatures for this method. Each signature is a tuple consisting of the return type followed by argument types. """ class AbstractRPCHandler(Component): implements(IXMLRPCHandler) abstract = True def _init_methods(self): import inspect self._rpc_methods = [] for name, val in inspect.getmembers(self): if hasattr(val, '_xmlrpc_signatures'): self._rpc_methods.append((val._xml_rpc_permission, val._xmlrpc_signatures, val, name)) def xmlrpc_methods(self): if not hasattr(self, '_rpc_methods'): self._init_methods() return self._rpc_methods class Method(object): """ Represents an XML-RPC exposed method. """ def __init__(self, provider, permission, signatures, callable, name = None): """ Accept a signature in the form returned by xmlrpc_methods. """ import pydoc self.permission = permission self.callable = callable self.rpc_signatures = signatures self.description = pydoc.getdoc(callable) if name is None: self.name = provider.xmlrpc_namespace() + '.' + callable.__name__ else: self.name = provider.xmlrpc_namespace() + '.' + name self.namespace = provider.xmlrpc_namespace() self.namespace_description = pydoc.getdoc(provider) def __call__(self, req, args): req.perm.assert_permission(self.permission) result = self.callable(req, *args) # If result is null, return a zero if result is None: result = 0 elif isinstance(result, dict): pass elif not isinstance(result, basestring): # Try and convert result to a list try: result = [i for i in result] except TypeError: pass return (result,) def _get_signature(self): """ Return the signature of this method. """ if hasattr(self, '_signature'): return self._signature fullargspec = inspect.getargspec(self.callable) argspec = fullargspec[0] assert argspec[0:2] == ['self', 'req'] or argspec[0] == 'req', \ 'Invalid argspec %s for %s' % (argspec, self.name) while argspec and (argspec[0] in ('self', 'req')): argspec.pop(0) argspec.reverse() defaults = fullargspec[3] if not defaults: defaults = [] else: defaults = list(defaults) args = [] sig = [] for sigcand in self.xmlrpc_signatures(): if len(sig) < len(sigcand): sig = sigcand sig = list(sig) for arg in argspec: if defaults: value = defaults.pop() if type(value) is str: if '"' in value: value = "'%s'" % value else: value = '"%s"' % value arg += '=%s' % value args.insert(0, RPC_TYPES[sig.pop()] + ' ' + arg) self._signature = '%s %s(%s)' % (RPC_TYPES[sig.pop()], self.name, ', '.join(args)) return self._signature signature = property(_get_signature) def xmlrpc_signatures(self): """ Signature as an XML-RPC 'signature'. """ return self.rpc_signatures class XMLRPCSystem(Component): """ Core of the XML-RPC system. """ implements(IPermissionRequestor, IXMLRPCHandler) method_handlers = ExtensionPoint(IXMLRPCHandler) # IPermissionRequestor methods def get_permission_actions(self): yield 'XML_RPC' # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'system' def xmlrpc_methods(self): yield ('XML_RPC', ((list, list),), self.multicall) yield ('XML_RPC', ((list,),), self.listMethods) yield ('XML_RPC', ((str, str),), self.methodHelp) yield ('XML_RPC', ((list, str),), self.methodSignature) yield ('XML_RPC', ((list,),), self.getAPIVersion) def get_method(self, method): """ Get an RPC signature by full name. """ for provider in self.method_handlers: for candidate in provider.xmlrpc_methods(): #self.env.log.debug(candidate) p = Method(provider, *candidate) if p.name == method: return p raise xmlrpclib.Fault(1, 'XML-RPC method "%s" not found' % method) # Exported methods def all_methods(self, req): """ List all methods exposed via XML-RPC. Returns a list of Method objects. """ for provider in self.method_handlers: for candidate in provider.xmlrpc_methods(): # Expand all fields of method description c = Method(provider, *candidate) if req.perm.has_permission(c.permission): yield c def multicall(self, req, signatures): """ Takes an array of XML-RPC calls encoded as structs of the form (in a Pythonish notation here): {'methodName': string, 'params': array} """ for signature in signatures: try: yield self.get_method(signature['methodName'])(req, signature['params']) except xmlrpclib.Fault, e: yield e except Exception, e: yield xmlrpclib.Fault(2, "'%s' while executing '%s()'" % (str(e), signature['methodName'])) def listMethods(self, req): """ This method returns a list of strings, one for each (non-system) method supported by the XML-RPC server. """ for method in self.all_methods(req): yield method.name def methodHelp(self, req, method): """ This method takes one parameter, the name of a method implemented by the XML-RPC server. It returns a documentation string describing the use of that method. If no such string is available, an empty string is returned. The documentation string may contain HTML markup. """ p = self.get_method(method) req.perm.assert_permission(p.permission) return '\n'.join((p.signature, '', p.description)) def methodSignature(self, req, method): """ This method takes one parameter, the name of a method implemented by the XML-RPC server. It returns an array of possible signatures for this method. A signature is an array of types. The first of these types is the return type of the method, the rest are parameters. """ p = self.get_method(method) req.perm.assert_permission(p.permission) return [','.join([RPC_TYPES[x] for x in sig]) for sig in p.xmlrpc_signatures()] def getAPIVersion(self, req): """ Returns a list with two elements. First element is the major version number, second is the minor. Changes to the major version indicate API breaking changes, while minor version changes are simple additions, bug fixes, etc. """ return [0, 2] xmlrpcplugin/0.10/setup.py0000644000175000017500000000065510503746773014050 0ustar wmbwmb#!/usr/bin/env python from setuptools import setup setup( name='TracXMLRPC', version='0.1', author='Alec Thomas', author_email='alec@swapoff.org', url='http://trac-hacks.swapoff.org/wiki/XmlRpcPlugin', description='XML-RPC interface to Trac', zip_safe=True, packages=['tracrpc'], package_data={'tracrpc': ['templates/*.cs']}, entry_points={'trac.plugins': 'TracXMLRPC = tracrpc'}, ) xmlrpcplugin/trunk/0000755000175000017500000000000012616345113013103 5ustar wmbwmbxmlrpcplugin/trunk/MANIFEST.in0000644000175000017500000000020011367407612014636 0ustar wmbwmbinclude README.wiki include setup.cfg include tracrpc/htdocs/*.css include tracrpc/htdocs/*.js include tracrpc/templates/*.html xmlrpcplugin/trunk/tracrpc/0000755000175000017500000000000012616345113014541 5ustar wmbwmbxmlrpcplugin/trunk/tracrpc/wiki.py0000644000175000017500000002262012542747004016063 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import os from datetime import datetime from trac.attachment import Attachment from trac.core import * from trac.mimeview import Context from trac.resource import Resource, ResourceNotFound from trac.wiki.api import WikiSystem, IWikiPageManipulator from trac.wiki.model import WikiPage from trac.wiki.formatter import wiki_to_html, format_to_html from tracrpc.api import IXMLRPCHandler, expose_rpc, Binary from tracrpc.util import StringIO, to_utimestamp, from_utimestamp __all__ = ['WikiRPC'] class WikiRPC(Component): """Superset of the [http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2 WikiRPC API]. """ implements(IXMLRPCHandler) manipulators = ExtensionPoint(IWikiPageManipulator) def __init__(self): self.wiki = WikiSystem(self.env) def xmlrpc_namespace(self): return 'wiki' def xmlrpc_methods(self): yield (None, ((dict, datetime),), self.getRecentChanges) yield ('WIKI_VIEW', ((int,),), self.getRPCVersionSupported) yield (None, ((str, str), (str, str, int),), self.getPage) yield (None, ((str, str, int),), self.getPage, 'getPageVersion') yield (None, ((str, str), (str, str, int)), self.getPageHTML) yield (None, ((str, str), (str, str, int)), self.getPageHTML, 'getPageHTMLVersion') yield (None, ((list,),), self.getAllPages) yield (None, ((dict, str), (dict, str, int)), self.getPageInfo) yield (None, ((dict, str, int),), self.getPageInfo, 'getPageInfoVersion') yield (None, ((bool, str, str, dict),), self.putPage) yield (None, ((list, str),), self.listAttachments) yield (None, ((Binary, str),), self.getAttachment) yield (None, ((bool, str, Binary),), self.putAttachment) yield (None, ((bool, str, str, str, Binary), (bool, str, str, str, Binary, bool)), self.putAttachmentEx) yield (None, ((bool, str),(bool, str, int)), self.deletePage) yield (None, ((bool, str),), self.deleteAttachment) yield ('WIKI_VIEW', ((list, str),), self.listLinks) yield ('WIKI_VIEW', ((str, str),), self.wikiToHtml) def _fetch_page(self, req, pagename, version=None): # Helper for getting the WikiPage that performs basic checks page = WikiPage(self.env, pagename, version) req.perm(page.resource).require('WIKI_VIEW') if page.exists: return page else: msg = 'Wiki page "%s" does not exist' % pagename if version is not None: msg += ' at version %s' % version raise ResourceNotFound(msg) def _page_info(self, name, when, author, version, comment): return dict(name=name, lastModified=when, author=author, version=int(version), comment=comment) def getRecentChanges(self, req, since): """ Get list of changed pages since timestamp """ since = to_utimestamp(since) wiki_realm = Resource('wiki') query = ('SELECT name, time, author, version, comment ' 'FROM wiki w1 ' 'WHERE time >= %s ' 'AND version = (SELECT MAX(version) ' ' FROM wiki w2 ' ' WHERE w2.name=w1.name) ' 'ORDER BY time DESC') if hasattr(self.env, 'db_query'): generator = self.env.db_query(query, (since,)) else: db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(query, (since,)) generator = cursor result = [] for name, when, author, version, comment in generator: if 'WIKI_VIEW' in req.perm(wiki_realm(id=name, version=version)): result.append( self._page_info(name, from_utimestamp(when), author, version, comment)) return result def getRPCVersionSupported(self, req): """ Returns 2 with this version of the Trac API. """ return 2 def getPage(self, req, pagename, version=None): """ Get the raw Wiki text of page, latest version. """ page = self._fetch_page(req, pagename, version) return page.text def getPageHTML(self, req, pagename, version=None): """ Return latest version of page as rendered HTML, utf8 encoded. """ page = self._fetch_page(req, pagename, version) fields = {'text': page.text} for manipulator in self.manipulators: manipulator.prepare_wiki_page(req, page, fields) context = Context.from_request(req, page.resource, absurls=True) html = format_to_html(self.env, context, fields['text']) return '%s' % html.encode('utf-8') def getAllPages(self, req): """ Returns a list of all pages. The result is an array of utf8 pagenames. """ pages = [] for page in self.wiki.get_pages(): if 'WIKI_VIEW' in req.perm(Resource('wiki', page)): pages.append(page) return pages def getPageInfo(self, req, pagename, version=None): """ Returns information about the given page. """ page = WikiPage(self.env, pagename, version) req.perm(page.resource).require('WIKI_VIEW') if page.exists: last_update = page.get_history().next() return self._page_info(page.name, last_update[1], last_update[2], page.version, page.comment) def putPage(self, req, pagename, content, attributes): """ writes the content of the page. """ page = WikiPage(self.env, pagename) if page.readonly: req.perm(page.resource).require('WIKI_ADMIN') elif not page.exists: req.perm(page.resource).require('WIKI_CREATE') else: req.perm(page.resource).require('WIKI_MODIFY') page.text = content if req.perm(page.resource).has_permission('WIKI_ADMIN'): page.readonly = attributes.get('readonly') and 1 or 0 page.save(attributes.get('author', req.authname), attributes.get('comment'), req.remote_addr) return True def deletePage(self, req, name, version=None): """Delete a Wiki page (all versions) or a specific version by including an optional version number. Attachments will also be deleted if page no longer exists. Returns True for success.""" wp = WikiPage(self.env, name, version) req.perm(wp.resource).require('WIKI_DELETE') try: wp.delete(version) return True except: return False def listAttachments(self, req, pagename): """ Lists attachments on a given page. """ for a in Attachment.select(self.env, 'wiki', pagename): if 'ATTACHMENT_VIEW' in req.perm(a.resource): yield pagename + '/' + a.filename def getAttachment(self, req, path): """ returns the content of an attachment. """ pagename, filename = os.path.split(path) attachment = Attachment(self.env, 'wiki', pagename, filename) req.perm(attachment.resource).require('ATTACHMENT_VIEW') return Binary(attachment.open().read()) def putAttachment(self, req, path, data): """ (over)writes an attachment. Returns True if successful. This method is compatible with WikiRPC. `putAttachmentEx` has a more extensive set of (Trac-specific) features. """ pagename, filename = os.path.split(path) self.putAttachmentEx(req, pagename, filename, None, data) return True def putAttachmentEx(self, req, pagename, filename, description, data, replace=True): """ Attach a file to a Wiki page. Returns the (possibly transformed) filename of the attachment. Use this method if you don't care about WikiRPC compatibility. """ if not WikiPage(self.env, pagename).exists: raise ResourceNotFound, 'Wiki page "%s" does not exist' % pagename if replace: try: attachment = Attachment(self.env, 'wiki', pagename, filename) req.perm(attachment.resource).require('ATTACHMENT_DELETE') attachment.delete() except TracError: pass attachment = Attachment(self.env, 'wiki', pagename) req.perm(attachment.resource).require('ATTACHMENT_CREATE') attachment.author = req.authname attachment.description = description attachment.insert(filename, StringIO(data.data), len(data.data)) return attachment.filename def deleteAttachment(self, req, path): """ Delete an attachment. """ pagename, filename = os.path.split(path) if not WikiPage(self.env, pagename).exists: raise ResourceNotFound, 'Wiki page "%s" does not exist' % pagename attachment = Attachment(self.env, 'wiki', pagename, filename) req.perm(attachment.resource).require('ATTACHMENT_DELETE') attachment.delete() return True def listLinks(self, req, pagename): """ ''Not implemented'' """ return [] def wikiToHtml(self, req, text): """ Render arbitrary Wiki text as HTML. """ return unicode(wiki_to_html(text, self.env, req, absurls=1)) xmlrpcplugin/trunk/tracrpc/web_ui.py0000644000175000017500000002123211540632324016363 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import sys from types import GeneratorType from pkg_resources import resource_filename from genshi.builder import tag from genshi.template.base import TemplateSyntaxError, BadDirectiveError from genshi.template.text import TextTemplate from trac.core import * from trac.perm import PermissionError from trac.resource import ResourceNotFound from trac.util.text import to_unicode from trac.util.translation import _ from trac.web.api import RequestDone, HTTPUnsupportedMediaType, \ HTTPInternalError from trac.web.main import IRequestHandler from trac.web.chrome import ITemplateProvider, INavigationContributor, \ add_stylesheet, add_script, add_ctxtnav from trac.wiki.formatter import wiki_to_oneliner from tracrpc.api import XMLRPCSystem, IRPCProtocol, ProtocolException, \ RPCError, ServiceException from tracrpc.util import accepts_mimetype __all__ = ['RPCWeb'] class RPCWeb(Component): """ Handle RPC calls from HTTP clients, as well as presenting a list of methods available to the currently logged in user. Browsing to /rpc or /login/rpc will display this list. """ implements(IRequestHandler, ITemplateProvider, INavigationContributor) protocols = ExtensionPoint(IRPCProtocol) # IRequestHandler methods def match_request(self, req): """ Look for available protocols serving at requested path and content-type. """ content_type = req.get_header('Content-Type') or 'text/html' must_handle_request = req.path_info in ('/rpc', '/login/rpc') for protocol in self.protocols: for p_path, p_type in protocol.rpc_match(): if req.path_info in ['/%s' % p_path, '/login/%s' % p_path]: must_handle_request = True if content_type.startswith(p_type): req.args['protocol'] = protocol return True # No protocol call, need to handle for docs or error if handled path return must_handle_request def process_request(self, req): protocol = req.args.get('protocol', None) content_type = req.get_header('Content-Type') or 'text/html' if protocol: # Perform the method call self.log.debug("RPC incoming request of content type '%s' " \ "dispatched to %s" % (content_type, repr(protocol))) self._rpc_process(req, protocol, content_type) elif accepts_mimetype(req, 'text/html') \ or content_type.startswith('text/html'): return self._dump_docs(req) else: # Attempt at API call gone wrong. Raise a plain-text 415 error body = "No protocol matching Content-Type '%s' at path '%s'." % ( content_type, req.path_info) self.log.error(body) req.send_error(None, template='', content_type='text/plain', status=HTTPUnsupportedMediaType.code, env=None, data=body) # Internal methods def _dump_docs(self, req): self.log.debug("Rendering docs") # Dump RPC documentation req.perm.require('XML_RPC') # Need at least XML_RPC namespaces = {} for method in XMLRPCSystem(self.env).all_methods(req): namespace = method.namespace.replace('.', '_') if namespace not in namespaces: namespaces[namespace] = { 'description' : wiki_to_oneliner( method.namespace_description, self.env, req=req), 'methods' : [], 'namespace' : method.namespace, } try: namespaces[namespace]['methods'].append( (method.signature, wiki_to_oneliner( method.description, self.env, req=req), method.permission)) except Exception, e: from tracrpc.util import StringIO import traceback out = StringIO() traceback.print_exc(file=out) raise Exception('%s: %s\n%s' % (method.name, str(e), out.getvalue())) add_stylesheet(req, 'common/css/wiki.css') add_stylesheet(req, 'tracrpc/rpc.css') add_script(req, 'tracrpc/rpc.js') return ('rpc.html', {'rpc': {'functions': namespaces, 'protocols': [p.rpc_info() + (list(p.rpc_match()),) \ for p in self.protocols], 'version': __import__('tracrpc', ['__version__']).__version__ }, 'expand_docs': self._expand_docs }, None) def _expand_docs(self, docs, ctx): try : tmpl = TextTemplate(docs) return tmpl.generate(**dict(ctx.items())).render() except (TemplateSyntaxError, BadDirectiveError), exc: self.log.exception("Syntax error rendering protocol documentation") return "'''Syntax error:''' [[BR]] %s" % (str(exc),) except Exception: self.log.exception("Runtime error rendering protocol documentation") return "Error rendering protocol documentation. " \ "Contact your '''Trac''' administrator for details" def _rpc_process(self, req, protocol, content_type): """Process incoming RPC request and finalize response.""" proto_id = protocol.rpc_info()[0] rpcreq = req.rpc = {'mimetype': content_type} try : self.log.debug("RPC(%s) call by '%s'", proto_id, req.authname) rpcreq = req.rpc = protocol.parse_rpc_request(req, content_type) rpcreq['mimetype'] = content_type # Important ! Check after parsing RPC request to add # protocol-specific fields in response # (e.g. JSON-RPC response `id`) req.perm.require('XML_RPC') # Need at least XML_RPC method_name = rpcreq.get('method') if method_name is None : raise ProtocolException('Missing method name') args = rpcreq.get('params') or [] self.log.debug("RPC(%s) call by '%s' %s", proto_id, \ req.authname, method_name) try : result = (XMLRPCSystem(self.env).get_method(method_name)(req, args))[0] if isinstance(result, GeneratorType): result = list(result) except (RPCError, PermissionError, ResourceNotFound), e: raise except Exception: e, tb = sys.exc_info()[-2:] raise ServiceException(e), None, tb else : protocol.send_rpc_result(req, result) except RequestDone : raise except (RPCError, PermissionError, ResourceNotFound), e: self.log.exception("RPC(%s) Error", proto_id) try : protocol.send_rpc_error(req, e) except RequestDone : raise except Exception, e : self.log.exception("RPC(%s) Unhandled protocol error", proto_id) self._send_unknown_error(req, e) except Exception, e : self.log.exception("RPC(%s) Unhandled protocol error", proto_id) self._send_unknown_error(req, e) def _send_unknown_error(self, req, e): """Last recourse if protocol cannot handle the RPC request | error""" method_name = req.rpc and req.rpc.get('method') or '(undefined)' body = "Unhandled protocol error calling '%s': %s" % ( method_name, to_unicode(e)) req.send_error(None, template='', content_type='text/plain', env=None, data=body, status=HTTPInternalError.code) # ITemplateProvider methods def get_htdocs_dirs(self): yield ('tracrpc', resource_filename(__name__, 'htdocs')) def get_templates_dirs(self): yield resource_filename(__name__, 'templates') # INavigationContributor methods def get_active_navigation_item(self, req): pass def get_navigation_items(self, req): if req.perm.has_permission('XML_RPC'): yield ('metanav', 'rpc', tag.a('API', href=req.href.rpc(), accesskey=1)) xmlrpcplugin/trunk/tracrpc/json_rpc.py0000644000175000017500000002432512146653314016741 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import datetime from itertools import izip import re from types import GeneratorType try: import babel except ImportError: babel = None import genshi from trac.core import * from trac.perm import PermissionError from trac.resource import ResourceNotFound from trac.util.datefmt import utc from trac.util.text import to_unicode from trac.web.api import RequestDone from tracrpc.api import IRPCProtocol, XMLRPCSystem, Binary, \ RPCError, MethodNotFound, ProtocolException from tracrpc.util import exception_to_unicode, empty, prepare_docs __all__ = ['JsonRpcProtocol'] try: import json if not (hasattr(json, 'JSONEncoder') \ and hasattr(json, 'JSONDecoder')): raise AttributeError("Incorrect JSON library found.") except (ImportError, AttributeError): try: import simplejson as json except ImportError: json = None __all__ = [] if json: class TracRpcJSONEncoder(json.JSONEncoder): """ Extending the JSON encoder to support some additional types: 1. datetime.datetime => {'__jsonclass__': ["datetime", ""]} 2. tracrpc.api.Binary => {'__jsonclass__': ["binary", ""]} 3. empty => '' 4. genshi.builder.Fragment|genshi.core.Markup => unicode 5. babel.support.LazyProxy => unicode """ def default(self, obj): if isinstance(obj, datetime.datetime): # http://www.ietf.org/rfc/rfc3339.txt return {'__jsonclass__': ["datetime", obj.strftime('%Y-%m-%dT%H:%M:%S')]} elif isinstance(obj, Binary): return {'__jsonclass__': ["binary", obj.data.encode("base64")]} elif obj is empty: return '' elif isinstance(obj, (genshi.builder.Fragment, genshi.core.Markup)): return unicode(obj) elif babel and isinstance(obj, babel.support.LazyProxy): return unicode(obj) else: return json.JSONEncoder(self, obj) class TracRpcJSONDecoder(json.JSONDecoder): """ Extending the JSON decoder to support some additional types: 1. {'__jsonclass__': ["datetime", ""]} => datetime.datetime 2. {'__jsonclass__': ["binary", ""]} => tracrpc.api.Binary """ dt = re.compile( '^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,}))?') def _normalize(self, obj): """ Helper to traverse JSON decoded object for custom types. """ if isinstance(obj, tuple): return tuple(self._normalize(item) for item in obj) elif isinstance(obj, list): return [self._normalize(item) for item in obj] elif isinstance(obj, dict): if obj.keys() == ['__jsonclass__']: kind, val = obj['__jsonclass__'] if kind == 'datetime': dt = self.dt.match(val) if not dt: raise Exception( "Invalid datetime string (%s)" % val) dt = tuple([int(i) for i in dt.groups() if i]) kw_args = {'tzinfo': utc} return datetime.datetime(*dt, **kw_args) elif kind == 'binary': try: bin = val.decode("base64") return Binary(bin) except: raise Exception("Invalid base64 string") else: raise Exception("Unknown __jsonclass__: %s" % kind) else: return dict(self._normalize(obj.items())) elif isinstance(obj, basestring): return to_unicode(obj) else: return obj def decode(self, obj, *args, **kwargs): obj = json.JSONDecoder.decode(self, obj, *args, **kwargs) return self._normalize(obj) class JsonProtocolException(ProtocolException): """Impossible to handle JSON-RPC request.""" def __init__(self, details, code=-32603, title=None, show_traceback=False): ProtocolException.__init__(self, details, title, show_traceback) self.code = code class JsonRpcProtocol(Component): r""" Example `POST` request using `curl` with `Content-Type` header and body: {{{ user: ~ > cat body.json {"params": ["WikiStart"], "method": "wiki.getPage", "id": 123} user: ~ > curl -H "Content-Type: application/json" --data @body.json ${req.abs_href.rpc()} {"id": 123, "error": null, "result": "= Welcome to.... }}} Implementation details: * JSON-RPC has no formalized type system, so a class-hint system is used for input and output of non-standard types: * `{"__jsonclass__": ["datetime", "YYYY-MM-DDTHH:MM:SS"]} => DateTime (UTC)` * `{"__jsonclass__": ["binary", ""]} => Binary` * `"id"` is optional, and any marker value received with a request is returned with the response. """ implements(IRPCProtocol) # IRPCProtocol methods def rpc_info(self): return ('JSON-RPC', prepare_docs(self.__doc__)) def rpc_match(self): yield('rpc', 'application/json') # Legacy path - provided for backwards compatibility: yield ('jsonrpc', 'application/json') def parse_rpc_request(self, req, content_type): """ Parse JSON-RPC requests""" if not json: self.log.debug("RPC(json) call ignored (not available).") raise JsonProtocolException("Error: JSON-RPC not available.\n") try: data = json.load(req, cls=TracRpcJSONDecoder) self.log.info("RPC(json) JSON-RPC request ID : %s.", data.get('id')) if data.get('method') == 'system.multicall': # Prepare for multicall self.log.debug("RPC(json) Multicall request %s", data) params = data.get('params', []) for signature in params : signature['methodName'] = signature.get('method', '') data['params'] = [params] return data except Exception, e: # Abort with exception - no data can be read self.log.error("RPC(json) decode error %s", exception_to_unicode(e, traceback=True)) raise JsonProtocolException(e, -32700) def send_rpc_result(self, req, result): """Send JSON-RPC response back to the caller.""" rpcreq = req.rpc r_id = rpcreq.get('id') try: if rpcreq.get('method') == 'system.multicall': # Custom multicall args = (rpcreq.get('params') or [[]])[0] mcresults = [self._json_result( isinstance(value, Exception) and \ value or value[0], \ sig.get('id') or r_id) \ for sig, value in izip(args, result)] response = self._json_result(mcresults, r_id) else: response = self._json_result(result, r_id) try: # JSON encoding self.log.debug("RPC(json) result: %s" % repr(response)) response = json.dumps(response, cls=TracRpcJSONEncoder) except Exception, e: response = json.dumps(self._json_error(e, r_id=r_id), cls=TracRpcJSONEncoder) except Exception, e: self.log.error("RPC(json) error %s" % exception_to_unicode(e, traceback=True)) response = json.dumps(self._json_error(e, r_id=r_id), cls=TracRpcJSONEncoder) self._send_response(req, response + '\n', rpcreq['mimetype']) def send_rpc_error(self, req, e): """Send a JSON-RPC fault message back to the caller. """ rpcreq = req.rpc r_id = rpcreq.get('id') response = json.dumps(self._json_error(e, r_id=r_id), \ cls=TracRpcJSONEncoder) self._send_response(req, response + '\n', rpcreq['mimetype']) # Internal methods def _send_response(self, req, response, content_type='application/json'): self.log.debug("RPC(json) encoded response: %s" % response) response = to_unicode(response).encode("utf-8") req.send_response(200) req.send_header('Content-Type', content_type) req.send_header('Content-Length', len(response)) req.end_headers() req.write(response) raise RequestDone() def _json_result(self, result, r_id=None): """ Create JSON-RPC response dictionary. """ if not isinstance(result, Exception): return {'result': result, 'error': None, 'id': r_id} else : return self._json_error(result, r_id=r_id) def _json_error(self, e, c=None, r_id=None): """ Makes a response dictionary that is an error. """ if isinstance(e, MethodNotFound): c = -32601 elif isinstance(e, PermissionError): c = 403 elif isinstance(e, ResourceNotFound): c = 404 else: c = c or hasattr(e, 'code') and e.code or -32603 return {'result': None, 'id': r_id, 'error': { 'name': hasattr(e, 'name') and e.name or 'JSONRPCError', 'code': c, 'message': to_unicode(e)}} xmlrpcplugin/trunk/tracrpc/templates/0000755000175000017500000000000012562637334016550 5ustar wmbwmbxmlrpcplugin/trunk/tracrpc/templates/rpc.html0000644000175000017500000000654312562637334020232 0ustar wmbwmb Remote Procedure Call (RPC)

Remote Procedure Call (RPC)

Installed API version : ${rpc.version}

Protocol reference:

Below you will find a detailed description of all the RPC protocols installed in this environment. This includes supported content types as well as target URLs for anonymous and authenticated access. Use this information to interact with this environment from a remote location.

Libraries for remote procedure calls and parsing exists for most major languages and platforms - use a tested, standard library for consistent results.

${protocol[0]}

For ${protocol[0]} protocol, use any one of:

${wiki_to_html(context, expand_docs(protocol[1], locals()['__data__']))}

RPC exported functions

${namespace.namespace} - ${namespace.description}

Function Description Permission required
${function[0]} ${function[1]} ${function[2] or "By resource"}
xmlrpcplugin/trunk/tracrpc/__init__.py0000644000175000017500000000123111367407612016654 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ from tracrpc.api import * from tracrpc.json_rpc import * from tracrpc.xml_rpc import * from tracrpc.web_ui import * from tracrpc.ticket import * from tracrpc.wiki import * from tracrpc.search import * __author__ = ['Alec Thomas ', 'Odd Simon Simonsen '] __license__ = 'BSD' try: __version__ = __import__('pkg_resources').get_distribution('TracXMLRPC').version except (ImportError, pkg_resources.DistributionNotFound): pass xmlrpcplugin/trunk/tracrpc/xml_rpc.py0000644000175000017500000002135212312150565016560 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import datetime import re import sys import time import xmlrpclib try: import babel except ImportError: babel = None import genshi from trac.core import * from trac.perm import PermissionError from trac.resource import ResourceNotFound from trac.util.datefmt import utc from trac.util.text import to_unicode from trac.web.api import RequestDone from tracrpc.api import XMLRPCSystem, IRPCProtocol, Binary, \ RPCError, MethodNotFound, ProtocolException, ServiceException from tracrpc.util import empty, prepare_docs __all__ = ['XmlRpcProtocol'] REPLACEMENT_CHAR = u'\uFFFD' # Unicode replacement character _illegal_unichrs = [(0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x1F), (0x7F, 0x84), (0x86, 0x9F), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF)] if sys.maxunicode >= 0x10000: # not narrow build _illegal_unichrs.extend([(0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF), (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF), (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF), (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF), (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF), (0x10FFFE, 0x10FFFF)]) _illegal_ranges = ["%s-%s" % (unichr(low), unichr(high)) for (low, high) in _illegal_unichrs] _illegal_xml_chars_RE = re.compile(u'[%s]' % u''.join(_illegal_ranges)) def to_xmlrpc_datetime(dt): """ Convert a datetime.datetime object to a xmlrpclib DateTime object """ return xmlrpclib.DateTime(dt.utctimetuple()) def from_xmlrpc_datetime(data): """Return datetime (in utc) from XMLRPC datetime string (is always utc)""" t = list(time.strptime(data.value, "%Y%m%dT%H:%M:%S")[0:6]) return apply(datetime.datetime, t, {'tzinfo': utc}) class XmlRpcProtocol(Component): r""" There should be XML-RPC client implementations available for all popular programming languages. Example call using `curl`: {{{ user: ~ > cat body.xml wiki.getPage WikiStart user: ~ > curl -H "Content-Type: application/xml" --data @body.xml ${req.abs_href.rpc()} = Welcome to.... }}} The following snippet illustrates how to perform authenticated calls in Python. {{{ >>> from xmlrpclib import ServerProxy >>> p = ServerProxy('${req.abs_href.login('rpc').replace('://', '://%s:your_password@' % authname)}') >>> p.system.getAPIVersion() [${', '.join(rpc.version.split('.'))}] }}} """ implements(IRPCProtocol) # IRPCProtocol methods def rpc_info(self): return ('XML-RPC', prepare_docs(self.__doc__)) def rpc_match(self): # Legacy path xmlrpc provided for backwards compatibility: # Using this order to get better docs yield ('rpc', 'application/xml') yield ('xmlrpc', 'application/xml') yield ('rpc', 'text/xml') yield ('xmlrpc', 'text/xml') def parse_rpc_request(self, req, content_type): """ Parse XML-RPC requests.""" try: args, method = xmlrpclib.loads( req.read(int(req.get_header('Content-Length')))) except Exception, e: self.log.debug("RPC(xml) parse error: %s", to_unicode(e)) raise ProtocolException(xmlrpclib.Fault(-32700, to_unicode(e))) else : self.log.debug("RPC(xml) call by '%s', method '%s' with args: %s" \ % (req.authname, method, repr(args))) args = self._normalize_xml_input(args) return {'method' : method, 'params' : args} def send_rpc_result(self, req, result): """Send the result of the XML-RPC call back to the client.""" rpcreq = req.rpc method = rpcreq.get('method') self.log.debug("RPC(xml) '%s' result: %s" % ( method, repr(result))) result = tuple(self._normalize_xml_output([result])) self._send_response(req, xmlrpclib.dumps(result, methodresponse=True), rpcreq['mimetype']) def send_rpc_error(self, req, e): """Send an XML-RPC fault message back to the caller""" rpcreq = req.rpc fault = None if isinstance(e, ProtocolException): fault = e._exc elif isinstance(e, ServiceException): e = e._exc elif isinstance(e, MethodNotFound): fault = xmlrpclib.Fault(-32601, to_unicode(e)) elif isinstance(e, PermissionError): fault = xmlrpclib.Fault(403, to_unicode(e)) elif isinstance(e, ResourceNotFound): fault = xmlrpclib.Fault(404, to_unicode(e)) if fault is not None : self._send_response(req, xmlrpclib.dumps(fault), rpcreq['mimetype']) else : self.log.error(e) import traceback from tracrpc.util import StringIO out = StringIO() traceback.print_exc(file = out) self.log.error(out.getvalue()) err_code = hasattr(e, 'code') and e.code or 1 method = rpcreq.get('method') self._send_response(req, xmlrpclib.dumps( xmlrpclib.Fault(err_code, "'%s' while executing '%s()'" % (str(e), method))), rpcreq['mimetype']) # Internal methods def _send_response(self, req, response, content_type='application/xml'): response = to_unicode(response) response = _illegal_xml_chars_RE.sub(REPLACEMENT_CHAR, response) response = response.encode("utf-8") req.send_response(200) req.send_header('Content-Type', content_type) req.send_header('Content-Length', len(response)) req.end_headers() req.write(response) raise RequestDone def _normalize_xml_input(self, args): """ Normalizes arguments (at any level - traversing dicts and lists): 1. xmlrpc.DateTime is converted to Python datetime 2. tracrpc.api.Binary => xmlrpclib.Binary 2. String line-endings same as from web (`\n` => `\r\n`) """ new_args = [] for arg in args: # self.env.log.debug("arg %s, type %s" % (arg, type(arg))) if isinstance(arg, xmlrpclib.DateTime): new_args.append(from_xmlrpc_datetime(arg)) elif isinstance(arg, xmlrpclib.Binary): arg.__class__ = Binary new_args.append(arg) elif isinstance(arg, basestring): new_args.append(arg.replace("\n", "\r\n")) elif isinstance(arg, dict): for key, val in arg.items(): arg[key], = self._normalize_xml_input([val]) new_args.append(arg) elif isinstance(arg, (list, tuple)): new_args.append(self._normalize_xml_input(arg)) else: new_args.append(arg) return new_args def _normalize_xml_output(self, result): """ Normalizes and converts output (traversing it): 1. None => '' 2. datetime => xmlrpclib.DateTime 3. Binary => xmlrpclib.Binary 4. genshi.builder.Fragment|genshi.core.Markup => unicode """ new_result = [] for res in result: if isinstance(res, datetime.datetime): new_result.append(to_xmlrpc_datetime(res)) elif isinstance(res, Binary): res.__class__ = xmlrpclib.Binary new_result.append(res) elif res is None or res is empty: new_result.append('') elif isinstance(res, (genshi.builder.Fragment, \ genshi.core.Markup)): new_result.append(to_unicode(res)) elif babel and isinstance(res, babel.support.LazyProxy): new_result.append(to_unicode(res)) elif isinstance(res, dict): for key, val in res.items(): res[key], = self._normalize_xml_output([val]) new_result.append(res) elif isinstance(res, list) or isinstance(res, tuple): new_result.append(self._normalize_xml_output(res)) else: new_result.append(res) return new_result xmlrpcplugin/trunk/tracrpc/tests/0000755000175000017500000000000012616345113015703 5ustar wmbwmbxmlrpcplugin/trunk/tracrpc/tests/wiki.py0000644000175000017500000001136111705146277017232 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import unittest import xmlrpclib import os import time from trac.util.compat import sorted from tracrpc.tests import rpc_testenv, TracRpcTestCase from tracrpc.util import StringIO class RpcWikiTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) def tearDown(self): TracRpcTestCase.tearDown(self) def test_attachments(self): # Note: Quite similar to the tracrpc.tests.json.JsonTestCase.test_binary image_url = os.path.join(rpc_testenv.trac_src, 'trac', 'htdocs', 'feed.png') image_in = StringIO(open(image_url, 'r').read()) # Create attachment self.admin.wiki.putAttachmentEx('TitleIndex', 'feed2.png', 'test image', xmlrpclib.Binary(image_in.getvalue())) self.assertEquals(image_in.getvalue(), self.admin.wiki.getAttachment( 'TitleIndex/feed2.png').data) # Update attachment (adding new) self.admin.wiki.putAttachmentEx('TitleIndex', 'feed2.png', 'test image', xmlrpclib.Binary(image_in.getvalue()), False) self.assertEquals(image_in.getvalue(), self.admin.wiki.getAttachment( 'TitleIndex/feed2.2.png').data) # List attachments self.assertEquals(['TitleIndex/feed2.2.png', 'TitleIndex/feed2.png'], sorted(self.admin.wiki.listAttachments('TitleIndex'))) # Delete both attachments self.admin.wiki.deleteAttachment('TitleIndex/feed2.png') self.admin.wiki.deleteAttachment('TitleIndex/feed2.2.png') # List attachments again self.assertEquals([], self.admin.wiki.listAttachments('TitleIndex')) def test_getRecentChanges(self): self.admin.wiki.putPage('WikiOne', 'content one', {}) time.sleep(1) self.admin.wiki.putPage('WikiTwo', 'content two', {}) attrs2 = self.admin.wiki.getPageInfo('WikiTwo') changes = self.admin.wiki.getRecentChanges(attrs2['lastModified']) self.assertEquals(1, len(changes)) self.assertEquals('WikiTwo', changes[0]['name']) self.assertEquals('admin', changes[0]['author']) self.assertEquals(1, changes[0]['version']) self.admin.wiki.deletePage('WikiOne') self.admin.wiki.deletePage('WikiTwo') def test_getPageHTMLWithImage(self): # Create the wiki page (absolute image reference) self.admin.wiki.putPage('ImageTest', '[[Image(wiki:ImageTest:feed.png, nolink)]]\n', {}) # Create attachment image_url = os.path.join(rpc_testenv.trac_src, 'trac', 'htdocs', 'feed.png') self.admin.wiki.putAttachmentEx('ImageTest', 'feed.png', 'test image', xmlrpclib.Binary(open(image_url, 'r').read())) # Check rendering absolute markup_1 = self.admin.wiki.getPageHTML('ImageTest') self.assertEquals('

\ntest image\n

\n', markup_1) # Change to relative image reference and check again self.admin.wiki.putPage('ImageTest', '[[Image(feed.png, nolink)]]\n', {}) markup_2 = self.admin.wiki.getPageHTML('ImageTest') self.assertEquals(markup_2, markup_1) def test_getPageHTMLWithManipulator(self): self.admin.wiki.putPage('FooBar', 'foo bar', {}) # Enable wiki manipulator plugin = os.path.join(rpc_testenv.tracdir, 'plugins', 'Manipulator.py') open(plugin, 'w').write( "from trac.core import *\n" "from trac.wiki.api import IWikiPageManipulator\n" "class WikiManipulator(Component):\n" " implements(IWikiPageManipulator)\n" " def prepare_wiki_page(self, req, page, fields):\n" " fields['text'] = 'foo bar baz'\n" " def validate_wiki_page(req, page):\n" " return []\n") rpc_testenv.restart() # Perform tests self.assertEquals('

\nfoo bar baz\n

\n', self.admin.wiki.getPageHTML('FooBar')) # Remove plugin and restart os.unlink(plugin) rpc_testenv.restart() def test_suite(): return unittest.makeSuite(RpcWikiTestCase) if __name__ == '__main__': unittest.main(defaultTest='test_suite') xmlrpcplugin/trunk/tracrpc/tests/web_ui.py0000644000175000017500000001125211375460767017546 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import unittest import urllib2 from tracrpc.tests import rpc_testenv, TracRpcTestCase class DocumentationTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() handler = urllib2.HTTPBasicAuthHandler(password_mgr) password_mgr.add_password(realm=None, uri=rpc_testenv.url_auth, user='user', passwd='user') self.opener_user = urllib2.build_opener(handler) def tearDown(self): TracRpcTestCase.tearDown(self) def test_get_with_content_type(self): req = urllib2.Request(rpc_testenv.url_auth, headers={'Content-Type': 'text/html'}) self.assert_rpcdocs_ok(self.opener_user, req) def test_get_no_content_type(self): req = urllib2.Request(rpc_testenv.url_auth) self.assert_rpcdocs_ok(self.opener_user, req) def test_post_accept(self): req = urllib2.Request(rpc_testenv.url_auth, headers={'Content-Type' : 'text/plain', 'Accept': 'application/x-trac-test,text/html'}, data='Pass since client accepts HTML') self.assert_rpcdocs_ok(self.opener_user, req) req = urllib2.Request(rpc_testenv.url_auth, headers={'Content-Type' : 'text/plain'}, data='Fail! No content type expected') self.assert_unsupported_media_type(self.opener_user, req) def test_form_submit(self): from urllib import urlencode # Explicit content type form_vars = {'result' : 'Fail! __FORM_TOKEN protection activated'} req = urllib2.Request(rpc_testenv.url_auth, headers={'Content-Type': 'application/x-www-form-urlencoded'}, data=urlencode(form_vars)) self.assert_form_protect(self.opener_user, req) # Implicit content type req = urllib2.Request(rpc_testenv.url_auth, headers={'Accept': 'application/x-trac-test,text/html'}, data='Pass since client accepts HTML') self.assert_form_protect(self.opener_user, req) def test_get_dont_accept(self): req = urllib2.Request(rpc_testenv.url_auth, headers={'Accept': 'application/x-trac-test'}) self.assert_unsupported_media_type(self.opener_user, req) def test_post_dont_accept(self): req = urllib2.Request(rpc_testenv.url_auth, headers={'Content-Type': 'text/plain', 'Accept': 'application/x-trac-test'}, data='Fail! Client cannot process HTML') self.assert_unsupported_media_type(self.opener_user, req) # Custom assertions def assert_rpcdocs_ok(self, opener, req): """Determine if RPC docs are ok""" try : resp = opener.open(req) except urllib2.HTTPError, e : self.fail("Request to '%s' failed (%s) %s" % (e.geturl(), e.code, e.fp.read())) else : self.assertEquals(200, resp.code) body = resp.read() self.assertTrue('

XML-RPC

' in body) self.assertTrue('

' in body) def assert_unsupported_media_type(self, opener, req): """Ensure HTTP 415 is returned back to the client""" try : opener.open(req) except urllib2.HTTPError, e: self.assertEquals(415, e.code) expected = "No protocol matching Content-Type '%s' at path '%s'." % \ (req.headers.get('Content-Type', 'text/plain'), '/login/rpc') got = e.fp.read() self.assertEquals(expected, got) except Exception, e: self.fail('Expected HTTP error but %s raised instead' % \ (e.__class__.__name__,)) else : self.fail('Expected HTTP error (415) but nothing raised') def assert_form_protect(self, opener, req): e = self.assertRaises(urllib2.HTTPError, opener.open, req) self.assertEquals(400, e.code) msg = e.fp.read() self.assertTrue("Missing or invalid form token. " "Do you have cookies enabled?" in msg) def test_suite(): return unittest.makeSuite(DocumentationTestCase) if __name__ == '__main__': unittest.main(defaultTest='test_suite') xmlrpcplugin/trunk/tracrpc/tests/json_rpc.py0000644000175000017500000002517512146653314020107 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import unittest import os import shutil import urllib2 from tracrpc.json_rpc import json from tracrpc.util import StringIO from tracrpc.tests import rpc_testenv, TracRpcTestCase class JsonModuleAvailabilityTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) def tearDown(self): TracRpcTestCase.tearDown(self) def test_json_not_available(self): if not json: # No json, so just make sure the protocol isn't there import tracrpc.json_rpc self.failIf(hasattr(tracrpc.json_rpc, 'JsonRpcProtocol'), "JsonRpcProtocol really available?") return # Module manipulation to simulate json libs not available import sys old_json = sys.modules.get('json', None) sys.modules['json'] = None old_simplejson = sys.modules.get('simplejson', None) sys.modules['simplejson'] = None if 'tracrpc.json_rpc' in sys.modules: del sys.modules['tracrpc.json_rpc'] try: import tracrpc.json_rpc self.failIf(hasattr(tracrpc.json_rpc, 'JsonRpcProtocol'), "JsonRpcProtocol really available?") finally: del sys.modules['json'] del sys.modules['simplejson'] if old_json: sys.modules['json'] = old_json if old_simplejson: sys.modules['simplejson'] = old_simplejson if 'tracrpc.json_rpc' in sys.modules: del sys.modules['tracrpc.json_rpc'] import tracrpc.json_rpc self.failIf(not hasattr(tracrpc.json_rpc, 'JsonRpcProtocol'), "What, no JsonRpcProtocol?") if not json: print "SKIP: json not available. Cannot run JsonTestCase." class JsonTestCase(TracRpcTestCase): pass else: class JsonTestCase(TracRpcTestCase): def _anon_req(self, data): req = urllib2.Request(rpc_testenv.url_anon, headers={'Content-Type': 'application/json'}) req.data = json.dumps(data) resp = urllib2.urlopen(req) return json.loads(resp.read()) def _auth_req(self, data, user='user'): password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() handler = urllib2.HTTPBasicAuthHandler(password_mgr) password_mgr.add_password(realm=None, uri=rpc_testenv.url_auth, user=user, passwd=user) req = urllib2.Request(rpc_testenv.url_auth, headers={'Content-Type': 'application/json'}) req.data = json.dumps(data) resp = urllib2.build_opener(handler).open(req) return json.loads(resp.read()) def setUp(self): TracRpcTestCase.setUp(self) def tearDown(self): TracRpcTestCase.tearDown(self) def test_call(self): result = self._anon_req( {'method': 'system.listMethods', 'params': [], 'id': 244}) self.assertTrue('system.methodHelp' in result['result']) self.assertEquals(None, result['error']) self.assertEquals(244, result['id']) def test_multicall(self): data = {'method': 'system.multicall', 'params': [ {'method': 'wiki.getAllPages', 'params': [], 'id': 1}, {'method': 'wiki.getPage', 'params': ['WikiStart', 1], 'id': 2}, {'method': 'ticket.status.getAll', 'params': [], 'id': 3}, {'method': 'nonexisting', 'params': []} ], 'id': 233} result = self._anon_req(data) self.assertEquals(None, result['error']) self.assertEquals(4, len(result['result'])) items = result['result'] self.assertEquals(1, items[0]['id']) self.assertEquals(233, items[3]['id']) self.assertTrue('WikiStart' in items[0]['result']) self.assertEquals(None, items[0]['error']) self.assertTrue('Welcome' in items[1]['result']) self.assertEquals(['accepted', 'assigned', 'closed', 'new', 'reopened'], items[2]['result']) self.assertEquals(None, items[3]['result']) self.assertEquals('JSONRPCError', items[3]['error']['name']) def test_datetime(self): # read and write datetime values from datetime import datetime from trac.util.datefmt import utc dt_str = "2009-06-19T16:46:00" dt_dt = datetime(2009, 06, 19, 16, 46, 00, tzinfo=utc) data = {'method': 'ticket.milestone.update', 'params': ['milestone1', {'due': {'__jsonclass__': ['datetime', dt_str]}}]} result = self._auth_req(data, user='admin') self.assertEquals(None, result['error']) result = self._auth_req({'method': 'ticket.milestone.get', 'params': ['milestone1']}, user='admin') self.assertTrue(result['result']) self.assertEquals(dt_str, result['result']['due']['__jsonclass__'][1]) def test_binary(self): # read and write binaries values image_url = os.path.join(rpc_testenv.trac_src, 'trac', 'htdocs', 'feed.png') image_in = StringIO(open(image_url, 'r').read()) data = {'method': 'wiki.putAttachmentEx', 'params': ['TitleIndex', "feed2.png", "test image", {'__jsonclass__': ['binary', image_in.getvalue().encode("base64")]}]} result = self._auth_req(data, user='admin') self.assertEquals('feed2.png', result['result']) # Now try to get the attachment, and verify it is identical result = self._auth_req({'method': 'wiki.getAttachment', 'params': ['TitleIndex/feed2.png']}, user='admin') self.assertTrue(result['result']) image_out = StringIO( result['result']['__jsonclass__'][1].decode("base64")) self.assertEquals(image_in.getvalue(), image_out.getvalue()) def test_fragment(self): data = {'method': 'ticket.create', 'params': ['ticket10786', '', {'type': 'enhancement', 'owner': 'A'}]} result = self._auth_req(data, user='admin') self.assertEquals(None, result['error']) tktid = result['result'] data = {'method': 'search.performSearch', 'params': ['ticket10786']} result = self._auth_req(data, user='admin') self.assertEquals(None, result['error']) self.assertEquals('#%d: enhancement: ' 'ticket10786 (new)' % tktid, result['result'][0][1]) self.assertEquals(1, len(result['result'])) data = {'method': 'ticket.delete', 'params': [tktid]} result = self._auth_req(data, user='admin') self.assertEquals(None, result['error']) def test_xmlrpc_permission(self): # Test returned response if not XML_RPC permission rpc_testenv._tracadmin('permission', 'remove', 'anonymous', 'XML_RPC', wait=True) try: result = self._anon_req({'method': 'system.listMethods', 'id': 'no-perm'}) self.assertEquals(None, result['result']) self.assertEquals('no-perm', result['id']) self.assertEquals(403, result['error']['code']) self.assertTrue('XML_RPC' in result['error']['message']) finally: # Add back the default permission for further tests rpc_testenv._tracadmin('permission', 'add', 'anonymous', 'XML_RPC', wait=True) def test_method_not_found(self): result = self._anon_req({'method': 'system.doesNotExist', 'id': 'no-method'}) self.assertTrue(result['error']) self.assertEquals(result['id'], 'no-method') self.assertEquals(None, result['result']) self.assertEquals(-32601, result['error']['code']) self.assertTrue('not found' in result['error']['message']) def test_wrong_argspec(self): result = self._anon_req({'method': 'system.listMethods', 'params': ['hello'], 'id': 'wrong-args'}) self.assertTrue(result['error']) self.assertEquals(result['id'], 'wrong-args') self.assertEquals(None, result['result']) self.assertEquals(-32603, result['error']['code']) self.assertTrue('listMethods() takes exactly 2 arguments' \ in result['error']['message']) def test_call_permission(self): # Test missing call-specific permission result = self._anon_req({'method': 'ticket.component.delete', 'params': ['component1'], 'id': 2332}) self.assertEquals(None, result['result']) self.assertEquals(2332, result['id']) self.assertEquals(403, result['error']['code']) self.assertTrue('TICKET_ADMIN privileges are required to ' 'perform this operation' in result['error']['message']) def test_resource_not_found(self): # A Ticket resource result = self._anon_req({'method': 'ticket.get', 'params': [2147483647], 'id': 3443}) self.assertEquals(result['id'], 3443) self.assertEquals(result['error']['code'], 404) self.assertEquals(result['error']['message'], 'Ticket 2147483647 does not exist.') # A Wiki resource result = self._anon_req({'method': 'wiki.getPage', 'params': ["Test", 10], 'id': 3443}) self.assertEquals(result['error']['code'], 404) self.assertEquals(result['error']['message'], 'Wiki page "Test" does not exist at version 10') def test_suite(): test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(JsonModuleAvailabilityTestCase)) test_suite.addTest(unittest.makeSuite(JsonTestCase)) return test_suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') xmlrpcplugin/trunk/tracrpc/tests/__init__.py0000644000175000017500000001157012147151166020022 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009-2013 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import unittest import os import time import urllib2 from tracrpc.util import PY24 try: from trac.tests.functional.svntestenv import SvnFunctionalTestEnvironment if not hasattr(SvnFunctionalTestEnvironment, 'init'): raise Exception("\nTrac version is out of date. " \ "Tests require minimum Trac 0.11.5dev r8303 to run.") class RpcTestEnvironment(SvnFunctionalTestEnvironment): def init(self): self.trac_src = os.path.realpath(os.path.join( __import__('trac', []).__file__, '..' , '..')) print "\nFound Trac source: %s" % self.trac_src SvnFunctionalTestEnvironment.init(self) self.url = "%s:%s" % (self.url, self.port) def post_create(self, env): print "Enabling RPC plugin and permissions..." env.config.set('components', 'tracrpc.*', 'enabled') env.config.save() self.getLogger = lambda : env.log self._tracadmin('permission', 'add', 'anonymous', 'XML_RPC') print "Created test environment: %s" % self.dirname parts = urllib2.urlparse.urlsplit(self.url) # Regular URIs self.url_anon = '%s://%s/rpc' % (parts[0], parts[1]) self.url_auth = '%s://%s/login/rpc' % (parts[0], parts[1]) # URIs with user:pass as part of URL self.url_user = '%s://user:user@%s/login/xmlrpc' % \ (parts[0], parts[1]) self.url_admin = '%s://admin:admin@%s/login/xmlrpc' % \ (parts[0], parts[1]) SvnFunctionalTestEnvironment.post_create(self, env) print "Starting web server: %s" % self.url self.restart() def _tracadmin(self, *args, **kwargs): do_wait = kwargs.pop('wait', False) SvnFunctionalTestEnvironment._tracadmin(self, *args, **kwargs) if do_wait: # Delay to ensure command executes and caches resets time.sleep(5) rpc_testenv = RpcTestEnvironment(os.path.realpath(os.path.join( os.path.realpath(__file__), '..', '..', '..', 'rpctestenv')), '8765', 'http://127.0.0.1') import atexit atexit.register(rpc_testenv.stop) def test_suite(): suite = unittest.TestSuite() import tracrpc.tests.api suite.addTest(tracrpc.tests.api.test_suite()) import tracrpc.tests.xml_rpc suite.addTest(tracrpc.tests.xml_rpc.test_suite()) import tracrpc.tests.json_rpc suite.addTest(tracrpc.tests.json_rpc.test_suite()) import tracrpc.tests.ticket suite.addTest(tracrpc.tests.ticket.test_suite()) import tracrpc.tests.wiki suite.addTest(tracrpc.tests.wiki.test_suite()) import tracrpc.tests.web_ui suite.addTest(tracrpc.tests.web_ui.test_suite()) import tracrpc.tests.search suite.addTest(tracrpc.tests.search.test_suite()) return suite except Exception, e: import sys, traceback traceback.print_exc(file=sys.stdout) print "Trac test infrastructure not available." print "Install Trac as 'python setup.py develop' (run Trac from source).\n" test_suite = unittest.TestSuite() # return empty suite TracRpcTestCase = unittest.TestCase else : __unittest = 1 # Do not show this module in tracebacks class TracRpcTestCase(unittest.TestCase): def setUp(self): log = rpc_testenv.get_trac_environment().log log.info('=' * 70) log.info('(TEST) Starting %s.%s', self.__class__.__name__, PY24 and getattr(self, '_TestCase__testMethodName') \ or getattr(self, '_testMethodName', '')) log.info('=' * 70) def failUnlessRaises(self, excClass, callableObj, *args, **kwargs): """Enhanced assertions to detect exceptions.""" try: callableObj(*args, **kwargs) except excClass, e: return e except self.failureException : raise except Exception, e : if hasattr(excClass, '__name__'): excName = excClass.__name__ else: excName = str(excClass) if hasattr(e, '__name__'): excMsg = e.__name__ else: excMsg = str(e) raise self.failureException("\n\nExpected %s\n\nGot %s : %s" % ( excName, e.__class__.__name__, excMsg)) else: if hasattr(excClass,'__name__'): excName = excClass.__name__ else: excName = str(excClass) raise self.failureException, "Expected %s\n\nNothing raised" % excName assertRaises = failUnlessRaises xmlrpcplugin/trunk/tracrpc/tests/xml_rpc.py0000644000175000017500000001417712542746257017747 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import os import sys import unittest import xmlrpclib from tracrpc.tests import rpc_testenv, TracRpcTestCase class RpcXmlTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) def tearDown(self): TracRpcTestCase.tearDown(self) def test_xmlrpc_permission(self): # Test returned response if not XML_RPC permission rpc_testenv._tracadmin('permission', 'remove', 'anonymous', 'XML_RPC', wait=True) e = self.assertRaises(xmlrpclib.Fault, self.anon.system.listMethods) self.assertEquals(403, e.faultCode) self.assertTrue('XML_RPC' in e.faultString) rpc_testenv._tracadmin('permission', 'add', 'anonymous', 'XML_RPC', wait=True) def test_method_not_found(self): def local_test(): self.admin.system.doesNotExist() self.fail("What? Method exists???") e = self.assertRaises(xmlrpclib.Fault, local_test) self.assertEquals(-32601, e.faultCode) self.assertTrue("not found" in e.faultString) def test_wrong_argspec(self): def local_test(): self.admin.system.listMethods("hello") self.fail("Oops. Wrong argspec accepted???") e = self.assertRaises(xmlrpclib.Fault, local_test) self.assertEquals(1, e.faultCode) self.assertTrue("listMethods() takes exactly 2 arguments" \ in e.faultString) def test_content_encoding(self): test_string = "øæåØÆÅàéüoö" # No encoding / encoding error def local_test(): t_id = self.admin.ticket.create(test_string, test_string[::-1], {}) self.admin.ticket.delete(t_id) self.fail("Expected ticket create to fail...") e = self.assertRaises(xmlrpclib.Fault, local_test) self.assertTrue(isinstance(e, xmlrpclib.Fault)) self.assertEquals(-32700, e.faultCode) # Unicode version (encodable) from trac.util.text import to_unicode test_string = to_unicode(test_string) t_id = self.admin.ticket.create(test_string, test_string[::-1], {}) self.assertTrue(t_id > 0) result = self.admin.ticket.get(t_id) self.assertEquals(result[0], t_id) self.assertEquals(result[3]['summary'], test_string) self.assertEquals(result[3]['description'], test_string[::-1]) self.assertEquals(unicode, type(result[3]['summary'])) self.admin.ticket.delete(t_id) def test_to_and_from_datetime(self): from datetime import datetime from trac.util.datefmt import to_datetime, utc from tracrpc.xml_rpc import to_xmlrpc_datetime, from_xmlrpc_datetime now = to_datetime(None, utc) now_timetuple = now.timetuple()[:6] xmlrpc_now = to_xmlrpc_datetime(now) self.assertTrue(isinstance(xmlrpc_now, xmlrpclib.DateTime), "Expected xmlprc_now to be an xmlrpclib.DateTime") self.assertEquals(str(xmlrpc_now), now.strftime("%Y%m%dT%H:%M:%S")) now_from_xmlrpc = from_xmlrpc_datetime(xmlrpc_now) self.assertTrue(isinstance(now_from_xmlrpc, datetime), "Expected now_from_xmlrpc to be a datetime") self.assertEquals(now_from_xmlrpc.timetuple()[:6], now_timetuple) self.assertEquals(now_from_xmlrpc.tzinfo, utc) def test_resource_not_found(self): # A Ticket resource e = self.assertRaises(xmlrpclib.Fault, self.admin.ticket.get, 2147483647) self.assertEquals(e.faultCode, 404) self.assertEquals(e.faultString, 'Ticket 2147483647 does not exist.') # A Wiki resource e = self.assertRaises(xmlrpclib.Fault, self.admin.wiki.getPage, "Test", 10) self.assertEquals(e.faultCode, 404) self.assertEquals(e.faultString, 'Wiki page "Test" does not exist at version 10') def test_xml_encoding_special_characters(self): tid1 = self.admin.ticket.create( 'One & Two < Four', 'Desc & ription\nLine 2', {}) ticket = self.admin.ticket.get(tid1) try: self.assertEquals('One & Two < Four', ticket[3]['summary']) self.assertEquals('Desc & ription\r\nLine 2', ticket[3]['description']) finally: self.admin.ticket.delete(tid1) def test_xml_encoding_invalid_characters(self): # Enable ticket manipulator plugin = os.path.join(rpc_testenv.tracdir, 'plugins', 'InvalidXmlCharHandler.py') open(plugin, 'w').write( "from trac.core import *\n" "from tracrpc.api import IXMLRPCHandler\n" "class UniChr(Component):\n" " implements(IXMLRPCHandler)\n" " def xmlrpc_namespace(self):\n" " return 'test_unichr'\n" " def xmlrpc_methods(self):\n" " yield ('XML_RPC', ((str, int),), self.unichr)\n" " def unichr(self, req, code):\n" " return (r'\U%08x' % code).decode('unicode-escape')\n") rpc_testenv.restart() from tracrpc.xml_rpc import _illegal_unichrs, REPLACEMENT_CHAR for low, high in _illegal_unichrs: for x in (low, (low + high) / 2, high): self.assertEquals(REPLACEMENT_CHAR, self.user.test_unichr.unichr(x), "Failed unichr with U+%04X" % (x,)) # surrogate pair on narrow build self.assertEquals(u'\U0001D4C1', self.user.test_unichr.unichr(0x1D4C1)) # Remove plugin and restart os.unlink(plugin) rpc_testenv.restart() def test_suite(): return unittest.makeSuite(RpcXmlTestCase) if __name__ == '__main__': unittest.main(defaultTest='test_suite') xmlrpcplugin/trunk/tracrpc/tests/search.py0000644000175000017500000000241612146654623017534 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2013 ::: Jun Omae (jun66j5@gmail.com) """ import unittest import xmlrpclib import os import shutil import datetime import time from tracrpc.tests import rpc_testenv, TracRpcTestCase class RpcSearchTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) def tearDown(self): TracRpcTestCase.tearDown(self) def test_fragment_in_search(self): t1 = self.admin.ticket.create("ticket10786", "", {'type': 'enhancement', 'owner': 'A'}) results = self.user.search.performSearch("ticket10786") self.assertEquals(1, len(results)) self.assertEquals('#%d: enhancement: ' 'ticket10786 (new)' % t1, results[0][1]) self.assertEquals(0, self.admin.ticket.delete(t1)) def test_suite(): test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(RpcSearchTestCase)) return test_suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') xmlrpcplugin/trunk/tracrpc/tests/ticket.py0000644000175000017500000005072012616345113017544 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009-2013 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import unittest import xmlrpclib import os import shutil import datetime import time from tracrpc.xml_rpc import to_xmlrpc_datetime from tracrpc.tests import rpc_testenv, TracRpcTestCase class RpcTicketTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) def tearDown(self): TracRpcTestCase.tearDown(self) def test_create_get_delete(self): tid = self.admin.ticket.create("create_get_delete", "fooy", {}) tid, time_created, time_changed, attributes = self.admin.ticket.get(tid) self.assertEquals('fooy', attributes['description']) self.assertEquals('create_get_delete', attributes['summary']) self.assertEquals('new', attributes['status']) self.assertEquals('admin', attributes['reporter']) self.admin.ticket.delete(tid) def test_getActions(self): tid = self.admin.ticket.create("ticket_getActions", "kjsald", {}) try: actions = self.admin.ticket.getActions(tid) finally: self.admin.ticket.delete(tid) default = [['leave', 'leave', '.', []], ['resolve', 'resolve', "The resolution will be set. Next status will be 'closed'.", [['action_resolve_resolve_resolution', 'fixed', ['fixed', 'invalid', 'wontfix', 'duplicate', 'worksforme']]]], ['reassign', 'reassign', "The owner will change from (none). Next status will be 'assigned'.", [['action_reassign_reassign_owner', 'admin', []]]], ['accept', 'accept', "The owner will change from (none) to admin. Next status will be 'accepted'.", []]] # Adjust for trac:changeset:9041 if 'will be changed' in actions[2][2]: default[2][2] = default[2][2].replace('will change', 'will be changed') default[3][2] = default[3][2].replace('will change', 'will be changed') # Adjust for trac:changeset:11777 if not 'from (none).' in actions[2][2]: default[2][2] = default[2][2].replace('from (none).', 'from (none) to the specified user.') # Adjust for trac:changeset:11778 if actions[0][2] != '.': default[0][2] = 'The ticket will remain with no owner.' # Adjust for trac:changeset:13203 if '(none)') default[3][2] = default[3][2].replace(' (none)', ' (none)') default[3][2] = default[3][2].replace(' admin', ' admin') self.assertEquals(actions, default) def test_getAvailableActions_DeleteTicket(self): # http://trac-hacks.org/ticket/5387 tid = self.admin.ticket.create('abc', 'def', {}) try: self.assertEquals(False, 'delete' in self.admin.ticket.getAvailableActions(tid)) env = rpc_testenv.get_trac_environment() delete_plugin = os.path.join(rpc_testenv.tracdir, 'plugins', 'DeleteTicket.py') shutil.copy(os.path.join( rpc_testenv.trac_src, 'sample-plugins', 'workflow', 'DeleteTicket.py'), delete_plugin) env.config.set('ticket', 'workflow', 'ConfigurableTicketWorkflow,DeleteTicketActionController') env.config.save() self.assertEquals(True, 'delete' in self.admin.ticket.getAvailableActions(tid)) self.assertEquals(False, 'delete' in self.user.ticket.getAvailableActions(tid)) env.config.set('ticket', 'workflow', 'ConfigurableTicketWorkflow') env.config.save() rpc_testenv.restart() self.assertEquals(False, 'delete' in self.admin.ticket.getAvailableActions(tid)) finally: # Clean up try: os.unlink(delete_plugin) except: pass rpc_testenv.restart() self.assertEquals(0, self.admin.ticket.delete(tid)) def test_FineGrainedSecurity(self): self.assertEquals(1, self.admin.ticket.create('abc', '123', {})) self.assertEquals(2, self.admin.ticket.create('def', '456', {})) # First some non-restricted tests for comparison: self.assertRaises(xmlrpclib.Fault, self.anon.ticket.create, 'abc', 'def') self.assertEquals([1,2], self.user.ticket.query()) self.assertTrue(self.user.ticket.get(2)) self.assertTrue(self.user.ticket.update(1, "ok")) self.assertTrue(self.user.ticket.update(2, "ok")) # Enable security policy and test from trac.core import Component, implements from trac.perm import IPermissionPolicy policy = os.path.join(rpc_testenv.tracdir, 'plugins', 'TicketPolicy.py') open(policy, 'w').write( "from trac.core import *\n" "from trac.perm import IPermissionPolicy\n" "class TicketPolicy(Component):\n" " implements(IPermissionPolicy)\n" " def check_permission(self, action, username, resource, perm):\n" " if username == 'user' and resource and resource.id == 2:\n" " return False\n" " if username == 'anonymous' and action == 'TICKET_CREATE':\n" " return True\n") env = rpc_testenv.get_trac_environment() _old_conf = env.config.get('trac', 'permission_policies') env.config.set('trac', 'permission_policies', 'TicketPolicy,'+_old_conf) env.config.save() rpc_testenv.restart() self.assertEquals([1], self.user.ticket.query()) self.assertTrue(self.user.ticket.get(1)) self.assertRaises(xmlrpclib.Fault, self.user.ticket.get, 2) self.assertTrue(self.user.ticket.update(1, "ok")) self.assertRaises(xmlrpclib.Fault, self.user.ticket.update, 2, "not ok") self.assertEquals(3, self.anon.ticket.create('efg', '789', {})) # Clean, reset and simple verification env.config.set('trac', 'permission_policies', _old_conf) env.config.save() os.unlink(policy) rpc_testenv.restart() self.assertEquals([1,2,3], self.user.ticket.query()) self.assertEquals(0, self.admin.ticket.delete(1)) self.assertEquals(0, self.admin.ticket.delete(2)) self.assertEquals(0, self.admin.ticket.delete(3)) def test_getRecentChanges(self): tid1 = self.admin.ticket.create("ticket_getRecentChanges", "one", {}) time.sleep(1) tid2 = self.admin.ticket.create("ticket_getRecentChanges", "two", {}) try: _id, created, modified, attributes = self.admin.ticket.get(tid2) changes = self.admin.ticket.getRecentChanges(created) self.assertEquals(changes, [tid2]) finally: self.admin.ticket.delete(tid1) self.admin.ticket.delete(tid2) def test_query_group_order_col(self): t1 = self.admin.ticket.create("1", "", {'type': 'enhancement', 'owner': 'A'}) t2 = self.admin.ticket.create("2", "", {'type': 'task', 'owner': 'B'}) t3 = self.admin.ticket.create("3", "", {'type': 'defect', 'owner': 'A'}) # order self.assertEquals([3,1,2], self.admin.ticket.query("order=type")) self.assertEquals([1,3,2], self.admin.ticket.query("order=owner")) self.assertEquals([2,1,3], self.admin.ticket.query("order=owner&desc=1")) # group self.assertEquals([1,3,2], self.admin.ticket.query("group=owner")) self.assertEquals([2,1,3], self.admin.ticket.query("group=owner&groupdesc=1")) # group + order self.assertEquals([2,3,1], self.admin.ticket.query("group=owner&groupdesc=1&order=type")) # col should just be ignored self.assertEquals([3,1,2], self.admin.ticket.query("order=type&col=status&col=reporter")) # clean self.assertEquals(0, self.admin.ticket.delete(t1)) self.assertEquals(0, self.admin.ticket.delete(t2)) self.assertEquals(0, self.admin.ticket.delete(t3)) def test_query_special_character_escape(self): # Note: This test only passes when using Trac 0.12+ # See http://trac-hacks.org/ticket/7737 if __import__('trac').__version__ < '0.12': self.fail("Known issue: Trac 0.11 does not handle escaped input properly.") summary = ["here&now", "maybe|later", "back\slash"] search = ["here\&now", "maybe\|later", "back\\slash"] tids = [] for s in summary: tids.append(self.admin.ticket.create(s, "test_special_character_escape", {})) try: for i in range(0, 3): self.assertEquals([tids[i]], self.admin.ticket.query("summary=%s" % search[i])) self.assertEquals(tids.sort(), self.admin.ticket.query("summary=%s" % "|".join(search)).sort()) finally: for tid in tids: self.admin.ticket.delete(tid) def test_update_author(self): tid = self.admin.ticket.create("ticket_update_author", "one", {}) self.admin.ticket.update(tid, 'comment1', {}) time.sleep(1) self.admin.ticket.update(tid, 'comment2', {}, False, 'foo') time.sleep(1) self.user.ticket.update(tid, 'comment3', {}, False, 'should_be_rejected') changes = self.admin.ticket.changeLog(tid) self.assertEquals(3, len(changes)) for when, who, what, cnum, comment, _tid in changes: self.assertTrue(comment in ('comment1', 'comment2', 'comment3')) if comment == 'comment1': self.assertEquals('admin', who) if comment == 'comment2': self.assertEquals('foo', who) if comment == 'comment3': self.assertEquals('user', who) self.admin.ticket.delete(tid) def test_create_at_time(self): from tracrpc.util import to_datetime, utc now = to_datetime(None, utc) minus1 = to_xmlrpc_datetime(now - datetime.timedelta(days=1)) # create the tickets (user ticket will not be permitted to change time) one = self.admin.ticket.create("create_at_time1", "ok", {}, False, minus1) two = self.user.ticket.create("create_at_time3", "ok", {}, False, minus1) # get the tickets t1 = self.admin.ticket.get(one) t2 = self.admin.ticket.get(two) # check timestamps self.assertTrue(t1[1] < t2[1]) self.admin.ticket.delete(one) self.admin.ticket.delete(two) def test_update_at_time(self): from tracrpc.util import to_datetime, utc now = to_datetime(None, utc) minus1 = to_xmlrpc_datetime(now - datetime.timedelta(hours=1)) minus2 = to_xmlrpc_datetime(now - datetime.timedelta(hours=2)) tid = self.admin.ticket.create("ticket_update_at_time", "ok", {}) self.admin.ticket.update(tid, 'one', {}, False, '', minus2) self.admin.ticket.update(tid, 'two', {}, False, '', minus1) self.user.ticket.update(tid, 'three', {}, False, '', minus1) time.sleep(1) self.user.ticket.update(tid, 'four', {}) changes = self.admin.ticket.changeLog(tid) self.assertEquals(4, len(changes)) # quick test to make sure each is older than previous self.assertTrue(changes[0][0] < changes[1][0] < changes[2][0]) # margin of 2 seconds for tests justnow = to_xmlrpc_datetime(now - datetime.timedelta(seconds=1)) self.assertTrue(justnow <= changes[2][0]) self.assertTrue(justnow <= changes[3][0]) self.admin.ticket.delete(tid) def test_update_non_existing(self): try: self.admin.ticket.update(3344, "a comment", {}) self.fail("Allowed to update non-existing ticket???") self.admin.ticket.delete(3234) except Exception, e: self.assertTrue("Ticket 3344 does not exist." in str(e)) def test_update_basic(self): import time # Basic update check, no 'action' or 'time_changed' tid = self.admin.ticket.create('test_update_basic1', 'ieidnsj', { 'owner': 'osimons'}) # old-style (deprecated) self.admin.ticket.update(tid, "comment1", {'component': 'component2'}) self.assertEquals(2, len(self.admin.ticket.changeLog(tid))) # new-style with 'action' time.sleep(1) # avoid "columns ticket, time, field are not unique" self.admin.ticket.update(tid, "comment2", {'component': 'component1', 'action': 'leave'}) self.assertEquals(4, len(self.admin.ticket.changeLog(tid))) self.admin.ticket.delete(tid) def test_update_time_changed(self): # Update with collision check import datetime from tracrpc.util import to_utimestamp from tracrpc.xml_rpc import from_xmlrpc_datetime tid = self.admin.ticket.create('test_update_time_changed', '...', {}) tid, created, modified, attrs = self.admin.ticket.get(tid) then = from_xmlrpc_datetime(modified) - datetime.timedelta(minutes=1) # Unrestricted old-style update (to be removed soon) try: self.admin.ticket.update(tid, "comment1", {'_ts': str(to_utimestamp(then))}) except Exception, e: self.assertTrue("Ticket has been updated since last get" in str(e)) # Update with 'action' to test new-style update. try: self.admin.ticket.update(tid, "comment1", {'_ts': str(to_utimestamp(then)), 'action': 'leave'}) except Exception, e: self.assertTrue("modified by someone else" in str(e)) self.admin.ticket.delete(tid) def test_update_time_same(self): # Unrestricted old-style update (to be removed soon) tid = self.admin.ticket.create('test_update_time_same', '...', {}) tid, created, modified, attrs = self.admin.ticket.get(tid) ts = attrs['_ts'] self.admin.ticket.update(tid, "comment1", {'_ts': ts}) self.admin.ticket.delete(tid) # Update with 'action' to test new-style update. tid = self.admin.ticket.create('test_update_time_same', '...', {}) tid, created, modified, attrs = self.admin.ticket.get(tid) ts = attrs['_ts'] self.admin.ticket.update(tid, "comment1", {'_ts': ts, 'action': 'leave'}) self.admin.ticket.delete(tid) def test_update_action(self): # Updating with 'action' in attributes tid = self.admin.ticket.create('test_update_action', 'ss') current = self.admin.ticket.get(tid) self.assertEqual('', current[3].get('owner', '')) updated = self.admin.ticket.update(tid, "comment1", {'action': 'reassign', 'action_reassign_reassign_owner': 'user'}) self.assertEqual('user', updated[3].get('owner')) self.admin.ticket.delete(tid) def test_update_action_non_existing(self): # Updating with non-existing 'action' in attributes tid = self.admin.ticket.create('test_update_action_wrong', 'ss') try: self.admin.ticket.update(tid, "comment1", {'action': 'reassign', 'action_reassign_reassign_owner': 'user'}) except Exception, e: self.assertTrue("invalid action" in str(e)) self.admin.ticket.delete(tid) def test_update_field_non_existing(self): tid = self.admin.ticket.create('test_update_field_non_existing', 'yw3') try: self.admin.ticket.update(tid, "comment1", {'does_not_exist': 'eiwrjoer'}) except Exception, e: self.assertTrue("no such column" in str(e)) self.admin.ticket.delete(tid) def test_create_ticket_9096(self): # See http://trac-hacks.org/ticket/9096 import urllib2, base64 body = """ ticket.create test summary test desc """ request = urllib2.Request(rpc_testenv.url + '/login/rpc', data=body) request.add_header('Content-Type', 'application/xml') request.add_header('Content-Length', str(len(body))) request.add_header('Authorization', 'Basic %s' \ % base64.encodestring('admin:admin')[:-1]) self.assertEquals('POST', request.get_method()) response = urllib2.urlopen(request) self.assertEquals(200, response.code) self.assertEquals("\n" "\n" "\n" "\n" "1\n" "\n" "\n" "\n", response.read()) self.admin.ticket.delete(1) def test_update_ticket_12430(self): # What if ticket 'time' and 'changetime' are part of attributes? # See https://trac-hacks.org/ticket/12430 tid1 = self.admin.ticket.create('test_update_ticket_12430', 'ok?', { 'owner': 'osimons1'}) try: # Get a fresh full copy tid2, created, changed, values = self.admin.ticket.get(tid1) self.assertTrue('time' in values, "'time' field not returned?") self.assertTrue('changetime' in values, "'changetime' field not returned?") self.assertTrue('_ts' in values, "No _ts in values?") # Update values['action'] = 'leave' values['owner'] = 'osimons2' self.admin.ticket.update(tid2, "updating", values) finally: self.admin.ticket.delete(tid1) class RpcTicketVersionTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) def tearDown(self): TracRpcTestCase.tearDown(self) def test_create(self): from tracrpc.util import to_datetime, utc dt = to_xmlrpc_datetime(to_datetime(None, utc)) desc = "test version" v = self.admin.ticket.version.create('9.99', {'time': dt, 'description': desc}) self.failUnless('9.99' in self.admin.ticket.version.getAll()) self.assertEquals({'time': dt, 'description': desc, 'name': '9.99'}, self.admin.ticket.version.get('9.99')) class RpcTicketTypeTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) def tearDown(self): TracRpcTestCase.tearDown(self) def test_getall_default(self): self.assertEquals(['defect', 'enhancement', 'task'], sorted(self.anon.ticket.type.getAll())) self.assertEquals(['defect', 'enhancement', 'task'], sorted(self.admin.ticket.type.getAll())) def test_suite(): test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(RpcTicketTestCase)) test_suite.addTest(unittest.makeSuite(RpcTicketVersionTestCase)) test_suite.addTest(unittest.makeSuite(RpcTicketTypeTestCase)) return test_suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') xmlrpcplugin/trunk/tracrpc/tests/api.py0000644000175000017500000001354312542746257017050 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2009-2013 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import os import unittest import urllib2 from tracrpc.tests import rpc_testenv, TracRpcTestCase from tracrpc.api import IRPCProtocol from trac.core import * from trac.test import Mock class ProtocolProviderTestCase(TracRpcTestCase): def setUp(self): TracRpcTestCase.setUp(self) def tearDown(self): TracRpcTestCase.tearDown(self) def test_invalid_content_type(self): req = urllib2.Request(rpc_testenv.url_anon, headers={'Content-Type': 'text/plain'}, data='Fail! No RPC for text/plain') try: resp = urllib2.urlopen(req) self.fail("Expected urllib2.HTTPError") except urllib2.HTTPError, e: self.assertEquals(e.code, 415) self.assertEquals(e.msg, "Unsupported Media Type") self.assertEquals(e.fp.read(), "No protocol matching Content-Type 'text/plain' at path '/rpc'.") def test_rpc_info(self): # Just try getting the docs for XML-RPC to test, it should always exist from tracrpc.xml_rpc import XmlRpcProtocol xmlrpc = XmlRpcProtocol(rpc_testenv.get_trac_environment()) name, docs = xmlrpc.rpc_info() self.assertEquals(name, 'XML-RPC') self.assertTrue('Content-Type: application/xml' in docs) def test_valid_provider(self): # Confirm the request won't work before adding plugin req = urllib2.Request(rpc_testenv.url_anon, headers={'Content-Type': 'application/x-tracrpc-test'}, data="Fail! No RPC for application/x-tracrpc-test") try: resp = urllib2.urlopen(req) self.fail("Expected urllib2.HTTPError") except urllib2.HTTPError, e: self.assertEquals(e.code, 415) # Make a new plugin provider = os.path.join(rpc_testenv.tracdir, 'plugins', 'DummyProvider.py') open(provider, 'w').write( "from trac.core import *\n" "from tracrpc.api import *\n" "class DummyProvider(Component):\n" " implements(IRPCProtocol)\n" " def rpc_info(self):\n" " return ('TEST-RPC', 'No Docs!')\n" " def rpc_match(self):\n" " yield ('rpc', 'application/x-tracrpc-test')\n" " def parse_rpc_request(self, req, content_type):\n" " return {'method' : 'system.getAPIVersion'}\n" " def send_rpc_error(self, req, e):\n" " rpcreq = req.rpc\n" " req.send_error(None, template='', content_type=rpcreq['mimetype'],\n" " status=500, env=None,\n" " data='Test failure: %s' % str(e))\n" " def send_rpc_result(self, req, result):\n" " rpcreq = req.rpc\n" " # raise KeyError('Here')\n" " response = 'Got a result!'\n" " req.send(response, rpcreq['mimetype'], 200)\n") rpc_testenv.restart() try: req = urllib2.Request(rpc_testenv.url_anon, headers={'Content-Type': 'application/x-tracrpc-test'}) resp = urllib2.urlopen(req) self.assertEquals(200, resp.code) self.assertEquals("Got a result!", resp.read()) self.assertEquals(resp.headers['Content-Type'], 'application/x-tracrpc-test;charset=utf-8') finally: # Clean up so that provider don't affect further tests os.unlink(provider) rpc_testenv.restart() def test_general_provider_error(self): # Make a new plugin and restart server provider = os.path.join(rpc_testenv.tracdir, 'plugins', 'DummyProvider.py') open(provider, 'w').write( "from trac.core import *\n" "from tracrpc.api import *\n" "class DummyProvider(Component):\n" " implements(IRPCProtocol)\n" " def rpc_info(self):\n" " return ('TEST-RPC', 'No Docs!')\n" " def rpc_match(self):\n" " yield ('rpc', 'application/x-tracrpc-test')\n" " def parse_rpc_request(self, req, content_type):\n" " return {'method' : 'system.getAPIVersion'}\n" " def send_rpc_error(self, req, e):\n" " if isinstance(e, RPCError) :\n" " req.send_error(None, template='', \n" " content_type='text/plain',\n" " status=500, env=None, data=e.message)\n" " else :\n" " req.send_error(None, template='', \n" " content_type='text/plain',\n" " status=500, env=None, data='Test failure')\n" " def send_rpc_result(self, req, result):\n" " raise RPCError('No good.')") rpc_testenv.restart() # Make the request try: try: req = urllib2.Request(rpc_testenv.url_anon, headers={'Content-Type': 'application/x-tracrpc-test'}) resp = urllib2.urlopen(req) except urllib2.HTTPError, e: self.assertEquals(500, e.code) self.assertEquals("No good.", e.fp.read()) self.assertTrue(e.hdrs['Content-Type'].startswith('text/plain')) finally: # Clean up so that provider don't affect further tests os.unlink(provider) rpc_testenv.restart() def test_suite(): return unittest.makeSuite(ProtocolProviderTestCase) if __name__ == '__main__': unittest.main(defaultTest='test_suite') xmlrpcplugin/trunk/tracrpc/util.py0000644000175000017500000000403312147123616016071 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009-2013 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import sys # Supported Python versions: PY24 = sys.version_info[:2] == (2, 4) PY25 = sys.version_info[:2] == (2, 5) PY26 = sys.version_info[:2] == (2, 6) PY27 = sys.version_info[:2] == (2, 7) from trac.util.compat import any try: from cStringIO import StringIO except ImportError: from StringIO import StringIO try: # Method only available in Trac 0.11.3 or higher from trac.util.text import exception_to_unicode except ImportError: def exception_to_unicode(e, traceback=""): from trac.util.text import to_unicode message = '%s: %s' % (e.__class__.__name__, to_unicode(e)) if traceback: from trac.util import get_last_traceback traceback_only = get_last_traceback().split('\n')[:-2] message = '\n%s\n%s' % (to_unicode('\n'.join(traceback_only)), message) return message try: # Constant available from Trac 0.12dev r8612 from trac.util.text import empty except ImportError: empty = None def accepts_mimetype(req, mimetype): if isinstance(mimetype, basestring): mimetype = (mimetype,) accept = req.get_header('Accept') if accept is None : # Don't make judgements if no MIME type expected and method is GET return req.method == 'GET' else : accept = accept.split(',') return any(x.strip().startswith(y) for x in accept for y in mimetype) def prepare_docs(text, indent=4): r"""Remove leading whitespace""" return text and ''.join(l[indent:] for l in text.splitlines(True)) or '' from trac.util.datefmt import to_datetime, utc try: # Micro-second support added to 0.12dev r9210 from trac.util.datefmt import to_utimestamp, from_utimestamp except ImportError: from trac.util.datefmt import to_timestamp to_utimestamp = to_timestamp from_utimestamp = lambda x: to_datetime(x, utc) xmlrpcplugin/trunk/tracrpc/htdocs/0000755000175000017500000000000011375457616016042 5ustar wmbwmbxmlrpcplugin/trunk/tracrpc/htdocs/rpc.js0000644000175000017500000000211011375457616017156 0ustar wmbwmb(function($) { $(document).ready(function () { // Create a Table of Contents (TOC) $('#content .wikipage') .prepend('

Contents

    '); function toc_entry(_this, item) { return $('
  • ' + _this.id.replace(/^rpc\./, '') + '
  • '); } var ul = $('#rpc-toc ul'); $("#content").find("*[id]").each(function(index, item) { var elem = undefined; if (this.tagName == 'H2') { elem = toc_entry(this, item); elem.css('padding-top', '0.5em'); } if (this.tagName == 'H3') { elem = toc_entry(this, item); elem.css('padding-left', '1.2em'); } ul.append(elem); }); $('#rpc-toc').toggle(); // Add anchors to headings $("#content").find("h2,h3").addAnchor("Link here"); }); })(jQuery); xmlrpcplugin/trunk/tracrpc/htdocs/rpc.css0000644000175000017500000000031111367407612017323 0ustar wmbwmb#rpc-toc { display: none; float: right; width: 15em; } #rpc-toc.wiki-toc h4 { text-align: center; } #rpc-toc ul { margin: 0; padding: 0.3em 0.15em; list-style-type: none; } xmlrpcplugin/trunk/tracrpc/search.py0000644000175000017500000000423711367407612016373 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ from trac.core import * from trac.search.api import ISearchSource from trac.search.web_ui import SearchModule from trac.util.compat import set from tracrpc.api import IXMLRPCHandler __all__ = ['SearchRPC'] class SearchRPC(Component): """ Search Trac. """ implements(IXMLRPCHandler) search_sources = ExtensionPoint(ISearchSource) # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'search' def xmlrpc_methods(self): yield ('SEARCH_VIEW', ((list,),), self.getSearchFilters) yield ('SEARCH_VIEW', ((list, str), (list, str, list)), self.performSearch) # Others def getSearchFilters(self, req): """ Retrieve a list of search filters with each element in the form (name, description). """ for source in self.search_sources: for filter in source.get_search_filters(req): yield filter def performSearch(self, req, query, filters=None): """ Perform a search using the given filters. Defaults to all if not provided. Results are returned as a list of tuples in the form (href, title, date, author, excerpt).""" query = SearchModule(self.env)._get_search_terms(query) filters_provided = filters is not None chosen_filters = set(filters or []) available_filters = [] for source in self.search_sources: available_filters += source.get_search_filters(req) filters = [f[0] for f in available_filters if f[0] in chosen_filters] if not filters: if filters_provided: return [] filters = [f[0] for f in available_filters] self.env.log.debug("Searching with %s" % filters) results = [] for source in self.search_sources: for result in source.get_search_results(req, query, filters): results.append(['/'.join(req.base_url.split('/')[0:3]) + result[0]] + list(result[1:])) return results xmlrpcplugin/trunk/tracrpc/ticket.py0000644000175000017500000005171112616345113016403 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import inspect from datetime import datetime import genshi from trac.attachment import Attachment from trac.core import * from trac.perm import PermissionError from trac.resource import Resource, ResourceNotFound import trac.ticket.model as model import trac.ticket.query as query from trac.ticket.api import TicketSystem from trac.ticket.notification import TicketNotifyEmail from trac.ticket.web_ui import TicketModule from trac.web.chrome import add_warning from trac.util.datefmt import to_datetime, utc from trac.util.text import to_unicode from tracrpc.api import IXMLRPCHandler, expose_rpc, Binary from tracrpc.util import StringIO, to_utimestamp, from_utimestamp __all__ = ['TicketRPC'] class TicketRPC(Component): """ An interface to Trac's ticketing system. """ implements(IXMLRPCHandler) # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'ticket' def xmlrpc_methods(self): yield (None, ((list,), (list, str)), self.query) yield (None, ((list, datetime),), self.getRecentChanges) yield (None, ((list, int),), self.getAvailableActions) yield (None, ((list, int),), self.getActions) yield (None, ((list, int),), self.get) yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool), (int, str, str, dict, bool, datetime)), self.create) yield (None, ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool), (list, int, str, dict, bool, str), (list, int, str, dict, bool, str, datetime)), self.update) yield (None, ((None, int),), self.delete) yield (None, ((dict, int), (dict, int, int)), self.changeLog) yield (None, ((list, int),), self.listAttachments) yield (None, ((Binary, int, str),), self.getAttachment) yield (None, ((str, int, str, str, Binary, bool), (str, int, str, str, Binary)), self.putAttachment) yield (None, ((bool, int, str),), self.deleteAttachment) yield ('TICKET_VIEW', ((list,),), self.getTicketFields) # Exported methods def query(self, req, qstr='status!=closed'): """ Perform a ticket query, returning a list of ticket ID's. All queries will use stored settings for maximum number of results per page and paging options. Use `max=n` to define number of results to receive, and use `page=n` to page through larger result sets. Using `max=0` will turn off paging and return all results. """ q = query.Query.from_string(self.env, qstr) ticket_realm = Resource('ticket') out = [] for t in q.execute(req): tid = t['id'] if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)): out.append(tid) return out def getRecentChanges(self, req, since): """Returns a list of IDs of tickets that have changed since timestamp.""" since = to_utimestamp(since) query = 'SELECT id FROM ticket WHERE changetime >= %s' if hasattr(self.env, 'db_query'): generator = self.env.db_query(query, (since,)) else: db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(query, (since,)) generator = cursor result = [] ticket_realm = Resource('ticket') for row in generator: tid = int(row[0]) if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)): result.append(tid) return result def getAvailableActions(self, req, id): """ Deprecated - will be removed. Replaced by `getActions()`. """ self.log.warning("Rpc ticket.getAvailableActions is deprecated") return [action[0] for action in self.getActions(req, id)] def getActions(self, req, id): """Returns the actions that can be performed on the ticket as a list of `[action, label, hints, [input_fields]]` elements, where `input_fields` is a list of `[name, value, [options]]` for any required action inputs.""" ts = TicketSystem(self.env) t = model.Ticket(self.env, id) actions = [] for action in ts.get_available_actions(req, t): fragment = genshi.builder.Fragment() hints = [] first_label = None for controller in ts.action_controllers: if action in [c_action for c_weight, c_action \ in controller.get_ticket_actions(req, t)]: label, widget, hint = \ controller.render_ticket_action_control(req, t, action) fragment += widget hints.append(to_unicode(hint).rstrip('.') + '.') first_label = first_label == None and label or first_label controls = [] for elem in fragment.children: if not isinstance(elem, genshi.builder.Element): continue if elem.tag == 'input': controls.append((elem.attrib.get('name'), elem.attrib.get('value'), [])) elif elem.tag == 'select': value = '' options = [] for opt in elem.children: if not (opt.tag == 'option' and opt.children): continue option = opt.children[0] options.append(option) if opt.attrib.get('selected'): value = option controls.append((elem.attrib.get('name'), value, options)) actions.append((action, first_label, " ".join(hints), controls)) return actions def get(self, req, id): """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ t = model.Ticket(self.env, id) req.perm(t.resource).require('TICKET_VIEW') t['_ts'] = str(to_utimestamp(t.time_changed)) return (t.id, t.time_created, t.time_changed, t.values) def create(self, req, summary, description, attributes={}, notify=False, when=None): """ Create a new ticket, returning the ticket ID. Overriding 'when' requires admin permission. """ t = model.Ticket(self.env) t['summary'] = summary t['description'] = description t['reporter'] = req.authname for k, v in attributes.iteritems(): t[k] = v t['status'] = 'new' t['resolution'] = '' # custom create timestamp? if when and not 'TICKET_ADMIN' in req.perm: self.log.warn("RPC ticket.create: %r not allowed to create with " "non-current timestamp (%r)", req.authname, when) when = None t.insert(when=when) if notify: try: tn = TicketNotifyEmail(self.env) tn.notify(t, newticket=True) except Exception, e: self.log.exception("Failure sending notification on creation " "of ticket #%s: %s" % (t.id, e)) return t.id def update(self, req, id, comment, attributes={}, notify=False, author='', when=None): """ Update a ticket, returning the new ticket in the same form as get(). 'New-style' call requires two additional items in attributes: (1) 'action' for workflow support (including any supporting fields as retrieved by getActions()), (2) '_ts' changetime token for detecting update collisions (as received from get() or update() calls). ''Calling update without 'action' and '_ts' changetime token is deprecated, and will raise errors in a future version.'' """ t = model.Ticket(self.env, id) # custom author? if author and not (req.authname == 'anonymous' \ or 'TICKET_ADMIN' in req.perm(t.resource)): # only allow custom author if anonymous is permitted or user is admin self.log.warn("RPC ticket.update: %r not allowed to change author " "to %r for comment on #%d", req.authname, author, id) author = '' author = author or req.authname # custom change timestamp? if when and not 'TICKET_ADMIN' in req.perm(t.resource): self.log.warn("RPC ticket.update: %r not allowed to update #%d with " "non-current timestamp (%r)", author, id, when) when = None when = when or to_datetime(None, utc) # never try to update 'time' and 'changetime' attributes directly if 'time' in attributes: del attributes['time'] if 'changetime' in attributes: del attributes['changetime'] # and action... if not 'action' in attributes: # FIXME: Old, non-restricted update - remove soon! self.log.warning("Rpc ticket.update for ticket %d by user %s " \ "has no workflow 'action'." % (id, req.authname)) req.perm(t.resource).require('TICKET_MODIFY') time_changed = attributes.pop('_ts', None) if time_changed and \ str(time_changed) != str(to_utimestamp(t.time_changed)): raise TracError("Ticket has been updated since last get().") for k, v in attributes.iteritems(): t[k] = v t.save_changes(author, comment, when=when) else: ts = TicketSystem(self.env) tm = TicketModule(self.env) # TODO: Deprecate update without time_changed timestamp time_changed = attributes.pop('_ts', to_utimestamp(t.time_changed)) try: time_changed = int(time_changed) except ValueError: raise TracError("RPC ticket.update: Wrong '_ts' token " \ "in attributes (%r)." % time_changed) action = attributes.get('action') avail_actions = ts.get_available_actions(req, t) if not action in avail_actions: raise TracError("Rpc: Ticket %d by %s " \ "invalid action '%s'" % (id, req.authname, action)) controllers = list(tm._get_action_controllers(req, t, action)) all_fields = [field['name'] for field in ts.get_ticket_fields()] for k, v in attributes.iteritems(): if k in all_fields and k != 'status': t[k] = v # TicketModule reads req.args - need to move things there... req.args.update(attributes) req.args['comment'] = comment # Collision detection: 0.11+0.12 timestamp req.args['ts'] = str(from_utimestamp(time_changed)) # Collision detection: 0.13/1.0+ timestamp req.args['view_time'] = str(time_changed) changes, problems = tm.get_ticket_changes(req, t, action) for warning in problems: add_warning(req, "Rpc ticket.update: %s" % warning) valid = problems and False or tm._validate_ticket(req, t) if not valid: raise TracError( " ".join([warning for warning in req.chrome['warnings']])) else: tm._apply_ticket_changes(t, changes) self.log.debug("Rpc ticket.update save: %s" % repr(t.values)) t.save_changes(author, comment, when=when) # Apply workflow side-effects for controller in controllers: controller.apply_action_side_effects(req, t, action) if notify: try: tn = TicketNotifyEmail(self.env) tn.notify(t, newticket=False, modtime=when) except Exception, e: self.log.exception("Failure sending notification on change of " "ticket #%s: %s" % (t.id, e)) return self.get(req, t.id) def delete(self, req, id): """ Delete ticket with the given id. """ t = model.Ticket(self.env, id) req.perm(t.resource).require('TICKET_ADMIN') t.delete() def changeLog(self, req, id, when=0): t = model.Ticket(self.env, id) req.perm(t.resource).require('TICKET_VIEW') for date, author, field, old, new, permanent in t.get_changelog(when): yield (date, author, field, old, new, permanent) # Use existing documentation from Ticket model changeLog.__doc__ = inspect.getdoc(model.Ticket.get_changelog) def listAttachments(self, req, ticket): """ Lists attachments for a given ticket. Returns (filename, description, size, time, author) for each attachment.""" attachments = [] for a in Attachment.select(self.env, 'ticket', ticket): if 'ATTACHMENT_VIEW' in req.perm(a.resource): yield (a.filename, a.description, a.size, a.date, a.author) def getAttachment(self, req, ticket, filename): """ returns the content of an attachment. """ attachment = Attachment(self.env, 'ticket', ticket, filename) req.perm(attachment.resource).require('ATTACHMENT_VIEW') return Binary(attachment.open().read()) def putAttachment(self, req, ticket, filename, description, data, replace=True): """ Add an attachment, optionally (and defaulting to) overwriting an existing one. Returns filename.""" if not model.Ticket(self.env, ticket).exists: raise ResourceNotFound('Ticket "%s" does not exist' % ticket) if replace: try: attachment = Attachment(self.env, 'ticket', ticket, filename) req.perm(attachment.resource).require('ATTACHMENT_DELETE') attachment.delete() except TracError: pass attachment = Attachment(self.env, 'ticket', ticket) req.perm(attachment.resource).require('ATTACHMENT_CREATE') attachment.author = req.authname attachment.description = description attachment.insert(filename, StringIO(data.data), len(data.data)) return attachment.filename def deleteAttachment(self, req, ticket, filename): """ Delete an attachment. """ if not model.Ticket(self.env, ticket).exists: raise ResourceNotFound('Ticket "%s" does not exists' % ticket) attachment = Attachment(self.env, 'ticket', ticket, filename) req.perm(attachment.resource).require('ATTACHMENT_DELETE') attachment.delete() return True def getTicketFields(self, req): """ Return a list of all ticket fields fields. """ return TicketSystem(self.env).get_ticket_fields() class StatusRPC(Component): """ An interface to Trac ticket status objects. Note: Status is defined by workflow, and all methods except `getAll()` are deprecated no-op methods - these will be removed later. """ implements(IXMLRPCHandler) # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'ticket.status' def xmlrpc_methods(self): yield ('TICKET_VIEW', ((list,),), self.getAll) yield ('TICKET_VIEW', ((dict, str),), self.get) yield ('TICKET_ADMIN', ((None, str,),), self.delete) yield ('TICKET_ADMIN', ((None, str, dict),), self.create) yield ('TICKET_ADMIN', ((None, str, dict),), self.update) def getAll(self, req): """ Returns all ticket states described by active workflow. """ return TicketSystem(self.env).get_all_status() def get(self, req, name): """ Deprecated no-op method. Do not use. """ # FIXME: Remove return '0' def delete(self, req, name): """ Deprecated no-op method. Do not use. """ # FIXME: Remove return 0 def create(self, req, name, attributes): """ Deprecated no-op method. Do not use. """ # FIXME: Remove return 0 def update(self, req, name, attributes): """ Deprecated no-op method. Do not use. """ # FIXME: Remove return 0 def ticketModelFactory(cls, cls_attributes): """ Return a class which exports an interface to trac.ticket.model.. """ class TicketModelImpl(Component): implements(IXMLRPCHandler) def xmlrpc_namespace(self): return 'ticket.' + cls.__name__.lower() def xmlrpc_methods(self): yield ('TICKET_VIEW', ((list,),), self.getAll) yield ('TICKET_VIEW', ((dict, str),), self.get) yield ('TICKET_ADMIN', ((None, str,),), self.delete) yield ('TICKET_ADMIN', ((None, str, dict),), self.create) yield ('TICKET_ADMIN', ((None, str, dict),), self.update) def getAll(self, req): for i in cls.select(self.env): yield i.name getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() def get(self, req, name): i = cls(self.env, name) attributes= {} for k, default in cls_attributes.iteritems(): v = getattr(i, k) if v is None: v = default attributes[k] = v return attributes get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() def delete(self, req, name): cls(self.env, name).delete() delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() def create(self, req, name, attributes): i = cls(self.env) i.name = name for k, v in attributes.iteritems(): setattr(i, k, v) i.insert(); create.__doc__ = """ Create a new ticket %s with the given attributes. """ % cls.__name__.lower() def update(self, req, name, attributes): self._updateHelper(name, attributes).update() update.__doc__ = """ Update ticket %s with the given attributes. """ % cls.__name__.lower() def _updateHelper(self, name, attributes): i = cls(self.env, name) for k, v in attributes.iteritems(): setattr(i, k, v) return i TicketModelImpl.__doc__ = """ Interface to ticket %s objects. """ % cls.__name__.lower() TicketModelImpl.__name__ = '%sRPC' % cls.__name__ return TicketModelImpl def ticketEnumFactory(cls): """ Return a class which exports an interface to one of the Trac ticket abstract enum types. """ class AbstractEnumImpl(Component): implements(IXMLRPCHandler) def xmlrpc_namespace(self): return 'ticket.' + cls.__name__.lower() def xmlrpc_methods(self): yield ('TICKET_VIEW', ((list,),), self.getAll) yield ('TICKET_VIEW', ((str, str),), self.get) yield ('TICKET_ADMIN', ((None, str,),), self.delete) yield ('TICKET_ADMIN', ((None, str, str),), self.create) yield ('TICKET_ADMIN', ((None, str, str),), self.update) def getAll(self, req): for i in cls.select(self.env): yield i.name getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() def get(self, req, name): if (cls.__name__ == 'Status'): i = cls(self.env) x = name else: i = cls(self.env, name) x = i.value return x get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() def delete(self, req, name): cls(self.env, name).delete() delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() def create(self, req, name, value): i = cls(self.env) i.name = name i.value = value i.insert() create.__doc__ = """ Create a new ticket %s with the given value. """ % cls.__name__.lower() def update(self, req, name, value): self._updateHelper(name, value).update() update.__doc__ = """ Update ticket %s with the given value. """ % cls.__name__.lower() def _updateHelper(self, name, value): i = cls(self.env, name) i.value = value return i AbstractEnumImpl.__doc__ = """ Interface to ticket %s. """ % cls.__name__.lower() AbstractEnumImpl.__name__ = '%sRPC' % cls.__name__ return AbstractEnumImpl ticketModelFactory(model.Component, {'name': '', 'owner': '', 'description': ''}) ticketModelFactory(model.Version, {'name': '', 'time': 0, 'description': ''}) ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''}) ticketEnumFactory(model.Type) ticketEnumFactory(model.Resolution) ticketEnumFactory(model.Priority) ticketEnumFactory(model.Severity) xmlrpcplugin/trunk/tracrpc/api.py0000644000175000017500000003233412077457611015701 0ustar wmbwmb# -*- coding: utf-8 -*- """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import inspect import types from datetime import datetime import xmlrpclib from trac.core import * from trac.perm import IPermissionRequestor __all__ = ['expose_rpc', 'IRPCProtocol', 'IXMLRPCHandler', 'AbstractRPCHandler', 'Method', 'XMLRPCSystem', 'Binary', 'RPCError', 'MethodNotFound', 'ProtocolException', 'ServiceException'] class Binary(xmlrpclib.Binary): """ RPC Binary type. Currently == xmlrpclib.Binary. """ pass #---------------------------------------------------------------- # RPC Exception classes #---------------------------------------------------------------- class RPCError(TracError): """ Error class for general RPC-related errors. """ class MethodNotFound(RPCError): """ Error to raise when requested method is not found. """ class _CompositeRpcError(RPCError): def __init__(self, details, title=None, show_traceback=False): if isinstance(details, Exception): self._exc = details message = unicode(details) else : self._exc = None message = details RPCError.__init__(self, message, title, show_traceback) def __unicode__(self): return u"%s details : %s" % (self.__class__.__name__, self.message) class ProtocolException(_CompositeRpcError): """Protocol could not handle RPC request. Usually this means that the request has some sort of syntactic error, a library needed to parse the RPC call is not available, or similar errors.""" class ServiceException(_CompositeRpcError): """The called method threw an exception. Helpful to identify bugs ;o)""" RPC_TYPES = {int: 'int', bool: 'boolean', str: 'string', float: 'double', datetime: 'DateTime', Binary: 'Binary', list: 'array', dict: 'struct', None : 'int'} def expose_rpc(permission, return_type, *arg_types): """ Decorator for exposing a method as an RPC call with the given signature. """ def decorator(func): if not hasattr(func, '_xmlrpc_signatures'): func._xmlrpc_signatures = [] func._xml_rpc_permission = permission func._xmlrpc_signatures.append((return_type,) + tuple(arg_types)) return func return decorator class IRPCProtocol(Interface): def rpc_info(): """ Returns a tuple of (name, docs). Method provides general information about the protocol used for the RPC HTML view. name: Shortname like 'XML-RPC'. docs: Documentation for the protocol. """ def rpc_match(): """ Return an iterable of (path_item, content_type) combinations that will be handled by the protocol. path_item: Single word to use for matching against (/login)?/. Answer to 'rpc' only if possible. content_type: Starts-with check of 'Content-Type' request header. """ def parse_rpc_request(req, content_type): """ Parse RPC requests. req : HTTP request object content_type : Input MIME type Return a dictionary with the following keys set. All the other values included in this mapping will be ignored by the core RPC subsystem, will be protocol-specific, and SHOULD NOT be needed in order to invoke a given method. method (MANDATORY): target method name (e.g. 'ticket.get') params (OPTIONAL) : a tuple containing input positional arguments headers (OPTIONAL) : if the protocol supports custom headers set by the client, then this value SHOULD be a dictionary binding `header name` to `value`. However, protocol handlers as well as target RPC methods *MUST (SHOULD ?) NOT* rely on specific values assigned to a particular header in order to send a response back to the client. mimetype : request MIME-type. This value will be set by core RPC components after calling this method so, please, ignore If the request cannot be parsed this method *MUST* raise an instance of `ProtocolException` optionally wrapping another exception containing details about the failure. """ def send_rpc_result(req, result): """Serialize the result of the RPC call and send it back to the client. req : Request object. The same mapping returned by `parse_rpc_request` can be accessed through `req.rpc` (see above). result : The value returned by the target RPC method """ def send_rpc_error(req, rpcreq, e): """Send a fault message back to the caller. Exception type and message are used for this purpose. This method *SHOULD* handle `RPCError`, `PermissionError`, `ResourceNotFound` and their subclasses. This method is *ALWAYS* called from within an exception handler. req : Request object. The same mapping returned by `parse_rpc_request` can be accessed through `req.rpc` (see above). e : exception object describing the failure """ class IXMLRPCHandler(Interface): def xmlrpc_namespace(): """ Provide the namespace in which a set of methods lives. This can be overridden if the 'name' element is provided by xmlrpc_methods(). """ def xmlrpc_methods(): """ Return an iterator of (permission, signatures, callable[, name]), where callable is exposed via XML-RPC if the authenticated user has the appropriate permission. The callable itself can be a method or a normal method. The first argument passed will always be a request object. The XMLRPCSystem performs some extra magic to remove the "self" and "req" arguments when listing the available methods. Signatures is a list of XML-RPC introspection signatures for this method. Each signature is a tuple consisting of the return type followed by argument types. """ class AbstractRPCHandler(Component): implements(IXMLRPCHandler) abstract = True def _init_methods(self): self._rpc_methods = [] for name, val in inspect.getmembers(self): if hasattr(val, '_xmlrpc_signatures'): self._rpc_methods.append((val._xml_rpc_permission, val._xmlrpc_signatures, val, name)) def xmlrpc_methods(self): if not hasattr(self, '_rpc_methods'): self._init_methods() return self._rpc_methods class Method(object): """ Represents an XML-RPC exposed method. """ def __init__(self, provider, permission, signatures, callable, name = None): """ Accept a signature in the form returned by xmlrpc_methods. """ self.permission = permission self.callable = callable self.rpc_signatures = signatures self.description = inspect.getdoc(callable) if name is None: self.name = provider.xmlrpc_namespace() + '.' + callable.__name__ else: self.name = provider.xmlrpc_namespace() + '.' + name self.namespace = provider.xmlrpc_namespace() self.namespace_description = inspect.getdoc(provider) def __call__(self, req, args): if self.permission: req.perm.assert_permission(self.permission) result = self.callable(req, *args) # If result is null, return a zero if result is None: result = 0 elif isinstance(result, dict): pass elif not isinstance(result, basestring): # Try and convert result to a list try: result = [i for i in result] except TypeError: pass return (result,) def _get_signature(self): """ Return the signature of this method. """ if hasattr(self, '_signature'): return self._signature fullargspec = inspect.getargspec(self.callable) argspec = fullargspec[0] assert argspec[0:2] == ['self', 'req'] or argspec[0] == 'req', \ 'Invalid argspec %s for %s' % (argspec, self.name) while argspec and (argspec[0] in ('self', 'req')): argspec.pop(0) argspec.reverse() defaults = fullargspec[3] if not defaults: defaults = [] else: defaults = list(defaults) args = [] sig = [] for sigcand in self.xmlrpc_signatures(): if len(sig) < len(sigcand): sig = sigcand sig = list(sig) for arg in argspec: if defaults: value = defaults.pop() if type(value) is str: if '"' in value: value = "'%s'" % value else: value = '"%s"' % value arg += '=%s' % value args.insert(0, RPC_TYPES[sig.pop()] + ' ' + arg) self._signature = '%s %s(%s)' % (RPC_TYPES[sig.pop()], self.name, ', '.join(args)) return self._signature signature = property(_get_signature) def xmlrpc_signatures(self): """ Signature as an XML-RPC 'signature'. """ return self.rpc_signatures class XMLRPCSystem(Component): """ Core of the RPC system. """ implements(IPermissionRequestor, IXMLRPCHandler) method_handlers = ExtensionPoint(IXMLRPCHandler) def __init__(self): self.env.systeminfo.append(('RPC', __import__('tracrpc', ['__version__']).__version__)) # IPermissionRequestor methods def get_permission_actions(self): yield 'XML_RPC' # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'system' def xmlrpc_methods(self): yield ('XML_RPC', ((list, list),), self.multicall) yield ('XML_RPC', ((list,),), self.listMethods) yield ('XML_RPC', ((str, str),), self.methodHelp) yield ('XML_RPC', ((list, str),), self.methodSignature) yield ('XML_RPC', ((list,),), self.getAPIVersion) def get_method(self, method): """ Get an RPC signature by full name. """ for provider in self.method_handlers: for candidate in provider.xmlrpc_methods(): #self.env.log.debug(candidate) p = Method(provider, *candidate) if p.name == method: return p raise MethodNotFound('RPC method "%s" not found' % method) # Exported methods def all_methods(self, req): """ List all methods exposed via RPC. Returns a list of Method objects. """ for provider in self.method_handlers: for candidate in provider.xmlrpc_methods(): # Expand all fields of method description yield Method(provider, *candidate) def multicall(self, req, signatures): """ Takes an array of RPC calls encoded as structs of the form (in a Pythonish notation here): `{'methodName': string, 'params': array}`. For JSON-RPC multicall, signatures is an array of regular method call structs, and result is an array of return structures. """ for signature in signatures: try: yield self.get_method(signature['methodName'])(req, signature['params']) except Exception, e: yield e def listMethods(self, req): """ This method returns a list of strings, one for each (non-system) method supported by the RPC server. """ for method in self.all_methods(req): yield method.name def methodHelp(self, req, method): """ This method takes one parameter, the name of a method implemented by the RPC server. It returns a documentation string describing the use of that method. If no such string is available, an empty string is returned. The documentation string may contain HTML markup. """ p = self.get_method(method) return '\n'.join((p.signature, '', p.description)) def methodSignature(self, req, method): """ This method takes one parameter, the name of a method implemented by the RPC server. It returns an array of possible signatures for this method. A signature is an array of types. The first of these types is the return type of the method, the rest are parameters. """ p = self.get_method(method) return [','.join([RPC_TYPES[x] for x in sig]) for sig in p.xmlrpc_signatures()] def getAPIVersion(self, req): """ Returns a list with three elements. First element is the epoch (0=Trac 0.10, 1=Trac 0.11 or higher). Second element is the major version number, third is the minor. Changes to the major version indicate API breaking changes, while minor version changes are simple additions, bug fixes, etc. """ import tracrpc return map(int, tracrpc.__version__.split('-')[0].split('.')) xmlrpcplugin/trunk/setup.cfg0000644000175000017500000000003012515204276014717 0ustar wmbwmb[egg_info] tag_build = xmlrpcplugin/trunk/setup.py0000644000175000017500000000172012616345113014615 0ustar wmbwmb#!/usr/bin/env python """ License: BSD (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) """ import sys from setuptools import setup, find_packages try : import crypt except ImportError : test_deps = ['twill', 'fcrypt'] else : test_deps = ['twill'] setup( name='TracXMLRPC', version='1.1.5', license='BSD', author='Alec Thomas', author_email='alec@swapoff.org', maintainer='Odd Simon Simonsen', maintainer_email='simon-code@bvnetwork.no', url='http://trac-hacks.org/wiki/XmlRpcPlugin', description='RPC interface to Trac', zip_safe=True, test_suite = 'tracrpc.tests.test_suite', tests_require = test_deps, packages=find_packages(exclude=['*.tests']), package_data={ 'tracrpc': ['templates/*.html', 'htdocs/*.js', 'htdocs/*.css'] }, entry_points={ 'trac.plugins': 'TracXMLRPC = tracrpc' }, ) xmlrpcplugin/trunk/README.wiki0000644000175000017500000000613411367407612014736 0ustar wmbwmb= Trac RPC plugin = Remote Procedure Call interface for Trac. Protocols: * XML-RPC * JSON-RPC API support: * search * system * ticket * ticket.component * ticket.milestone * ticket.priority * ticket.resolution * ticket.severity * ticket.status * ticket.type * ticket.version * wiki == Installing and Using == See http://trac-hacks.org/wiki/XmlRpcPlugin for details on how to install, how get help, and how to report issues. == API Documentation == The API documentation is available at `/rpc` for projects that have the plugin installed and enabled. It can be accessed by all users that have been granted `XML_RPC` permission. == Development == The Trac RPC plugin uses pluggable interfaces to do all its work. That means it is easy to extend, and currently supports: * protocols; add a new protocol in addition to the builtin ones and read input and answer request in whatever form and format needed. * methods; adding new methods available for remote procedure calls that will work for any enabled protocol. See source for documentation. The source code can be obtained from: http://trac-hacks.org/svn/xmlrpcplugin/ For work on the plugin itself (for submitting patches and more), please verify patches by running unittests (requires Trac source code on path): {{{ python setup.py test }}} == Thanks == Thanks to all those that use the plugin, and contribute with error reports, and patches for bugs and enhancements. Special thanks to: * Matt Good * Steffen Pingel * Olemis Lang == License == {{{ Copyright (c) 2005-2008, Alec Thomas (alec@swapoff.org) Copyright (c) 2009, CodeResort.com/BV Network AS (simon-code@bvnetwork.no) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder(s) nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. }}}