xmlrpcplugin/ 0000755 0001750 0001750 00000000000 12745672763 011761 5 ustar wmb wmb xmlrpcplugin/0.10/ 0000755 0001750 0001750 00000000000 10601605463 012315 5 ustar wmb wmb xmlrpcplugin/0.10/tracrpc/ 0000755 0001750 0001750 00000000000 10563613640 013756 5 ustar wmb wmb xmlrpcplugin/0.10/tracrpc/wiki.py 0000644 0001750 0001750 00000016212 10543501252 015266 0 ustar wmb wmb try:
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.py 0000644 0001750 0001750 00000006263 10472454454 015616 0 ustar wmb wmb from 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/ 0000755 0001750 0001750 00000000000 10525202651 015746 5 ustar wmb wmb xmlrpcplugin/0.10/tracrpc/templates/xmlrpclist.cs 0000644 0001750 0001750 00000001751 10525202651 020502 0 ustar wmb wmb
XML-RPC exported functions
> -
-
Function |
Description |
Permission required |
| | |
xmlrpcplugin/0.10/tracrpc/__init__.py 0000644 0001750 0001750 00000000214 10356104531 016056 0 ustar wmb wmb from 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.py 0000644 0001750 0001750 00000000340 10543501252 015273 0 ustar wmb wmb import 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.py 0000644 0001750 0001750 00000003620 10503746773 015606 0 ustar wmb wmb from 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.py 0000644 0001750 0001750 00000025777 10563613640 015635 0 ustar wmb wmb from 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.py 0000644 0001750 0001750 00000020750 10563613640 015105 0 ustar wmb wmb from 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.py 0000644 0001750 0001750 00000000655 10503746773 014050 0 ustar wmb wmb #!/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/ 0000755 0001750 0001750 00000000000 12616345113 013103 5 ustar wmb wmb xmlrpcplugin/trunk/MANIFEST.in 0000644 0001750 0001750 00000000200 11367407612 014636 0 ustar wmb wmb include README.wiki
include setup.cfg
include tracrpc/htdocs/*.css
include tracrpc/htdocs/*.js
include tracrpc/templates/*.html
xmlrpcplugin/trunk/tracrpc/ 0000755 0001750 0001750 00000000000 12616345113 014541 5 ustar wmb wmb xmlrpcplugin/trunk/tracrpc/wiki.py 0000644 0001750 0001750 00000022620 12542747004 016063 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000021232 11540632324 016363 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000024325 12146653314 016741 0 ustar wmb wmb # -*- 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/ 0000755 0001750 0001750 00000000000 12562637334 016550 5 ustar wmb wmb xmlrpcplugin/trunk/tracrpc/templates/rpc.html 0000644 0001750 0001750 00000006543 12562637334 020232 0 ustar wmb wmb
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:
-
{'Content-Type': '$ct'} header with request to:
${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__.py 0000644 0001750 0001750 00000001231 11367407612 016654 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000021352 12312150565 016560 0 ustar wmb wmb # -*- 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/ 0000755 0001750 0001750 00000000000 12616345113 015703 5 ustar wmb wmb xmlrpcplugin/trunk/tracrpc/tests/wiki.py 0000644 0001750 0001750 00000011361 11705146277 017232 0 ustar wmb wmb # -*- 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('\n
\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.py 0000644 0001750 0001750 00000011252 11375460767 017546 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000025175 12146653314 020107 0 ustar wmb wmb # -*- 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__.py 0000644 0001750 0001750 00000011570 12147151166 020022 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000014177 12542746257 017747 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000002416 12146654623 017534 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000050720 12616345113 017544 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000013543 12542746257 017050 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000004033 12147123616 016071 0 ustar wmb wmb # -*- 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/ 0000755 0001750 0001750 00000000000 11375457616 016042 5 ustar wmb wmb xmlrpcplugin/trunk/tracrpc/htdocs/rpc.js 0000644 0001750 0001750 00000002110 11375457616 017156 0 ustar wmb wmb (function($) {
$(document).ready(function () {
// Create a Table of Contents (TOC)
$('#content .wikipage')
.prepend('');
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.css 0000644 0001750 0001750 00000000311 11367407612 017323 0 ustar wmb wmb #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.py 0000644 0001750 0001750 00000004237 11367407612 016373 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000051711 12616345113 016403 0 ustar wmb wmb # -*- 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.py 0000644 0001750 0001750 00000032334 12077457611 015701 0 ustar wmb wmb # -*- 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.cfg 0000644 0001750 0001750 00000000030 12515204276 014717 0 ustar wmb wmb [egg_info]
tag_build =
xmlrpcplugin/trunk/setup.py 0000644 0001750 0001750 00000001720 12616345113 014615 0 ustar wmb wmb #!/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.wiki 0000644 0001750 0001750 00000006134 11367407612 014736 0 ustar wmb wmb = 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.
}}}