Back to index

moin  1.9.0~rc2
__init__.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Wiki XMLRPC v1 and v2 Interface + plugin extensions
00004 
00005     Parts of this code are based on Juergen Hermann's wikirpc.py,
00006     Les Orchard's "xmlrpc.cgi" and further work by Gustavo Niemeyer.
00007 
00008     See http://www.ecyrd.com/JSPWiki/Wiki.jsp?page=WikiRPCInterface
00009     and http://www.decafbad.com/twiki/bin/view/Main/XmlRpcToWiki
00010     for specs on many of the functions here.
00011 
00012     See also http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2
00013     for the new stuff.
00014 
00015     The main difference between v1 and v2 is that v2 relies on utf-8
00016     as transport encoding. No url-encoding and no base64 anymore, except
00017     when really necessary (like for transferring binary files like
00018     attachments maybe).
00019 
00020     @copyright: 2003-2008 MoinMoin:ThomasWaldmann,
00021                 2004-2006 MoinMoin:AlexanderSchremmer
00022     @license: GNU GPL, see COPYING for details
00023 """
00024 from MoinMoin.util import pysupport
00025 
00026 modules = pysupport.getPackageModules(__file__)
00027 
00028 import os, sys, time, xmlrpclib
00029 
00030 from MoinMoin import log
00031 logging = log.getLogger(__name__)
00032 
00033 from MoinMoin import auth, config, user, wikiutil
00034 from MoinMoin.Page import Page
00035 from MoinMoin.PageEditor import PageEditor
00036 from MoinMoin.logfile import editlog
00037 from MoinMoin.action import AttachFile
00038 from MoinMoin import caching
00039 
00040 
00041 logging_tearline = '- XMLRPC %s ' + '-' * 40
00042 
00043 class XmlRpcBase:
00044     """ XMLRPC base class with common functionality of wiki xmlrpc v1 and v2 """
00045     def __init__(self, request):
00046         """
00047         Initialize an XmlRpcBase object.
00048         @param request: the request object
00049         """
00050         self.request = request
00051         self.version = None # this has to be defined in derived class
00052         self.cfg = request.cfg
00053 
00054     #############################################################################
00055     ### Helper functions
00056     #############################################################################
00057 
00058     def _instr(self, text):
00059         """ Convert inbound string.
00060 
00061         @param text: the text to convert (encoded str or unicode)
00062         @rtype: unicode
00063         @return: text as unicode
00064         """
00065         raise NotImplementedError("please implement _instr in derived class")
00066 
00067     def _outstr(self, text):
00068         """ Convert outbound string.
00069 
00070         @param text: the text to convert (encoded str or unicode)
00071         @rtype: str
00072         @return: text as encoded str
00073         """
00074         raise NotImplementedError("please implement _outstr in derived class")
00075 
00076     def _inlob(self, text):
00077         """ Convert inbound base64-encoded utf-8 to Large OBject.
00078 
00079         @param text: the text to convert
00080         @rtype: unicode
00081         @return: text
00082         """
00083         text = text.data #this is a already base64-decoded 8bit string
00084         text = unicode(text, 'utf-8')
00085         return text
00086 
00087     def _outlob(self, text):
00088         """ Convert outbound Large OBject to base64-encoded utf-8.
00089 
00090         @param text: the text, either unicode or utf-8 string
00091         @rtype: str
00092         @return: xmlrpc Binary object
00093         """
00094         if isinstance(text, unicode):
00095             text = text.encode('utf-8')
00096         else:
00097             if config.charset != 'utf-8':
00098                 text = unicode(text, config.charset).encode('utf-8')
00099         return xmlrpclib.Binary(text)
00100 
00101     def _dump_exc(self):
00102         """ Convert an exception to a string.
00103 
00104         @rtype: str
00105         @return: traceback as string
00106         """
00107         import traceback
00108 
00109         return "%s: %s\n%s" % (
00110             sys.exc_info()[0],
00111             sys.exc_info()[1],
00112             '\n'.join(traceback.format_tb(sys.exc_info()[2])),
00113         )
00114 
00115     def process(self):
00116         """ xmlrpc v1 and v2 dispatcher """
00117         request = self.request
00118         try:
00119             if 'xmlrpc' in self.request.cfg.actions_excluded:
00120                 # we do not handle xmlrpc v1 and v2 differently
00121                 response = xmlrpclib.Fault(1, "This moin wiki does not allow xmlrpc method calls.")
00122             else:
00123                 data = request.in_data
00124 
00125                 try:
00126                     params, method = xmlrpclib.loads(data)
00127                 except:
00128                     # if anything goes wrong here, we want to see the raw data:
00129                     logging.debug("Length of raw data: %d bytes" % len(data))
00130                     logging.debug(logging_tearline % 'request raw data begin')
00131                     logging.debug('%r' % data)
00132                     logging.debug(logging_tearline % 'request raw data end')
00133                     raise
00134 
00135                 logging.debug(logging_tearline % 'request parsed data begin')
00136                 logging.debug('%s(%r)' % (method, params))
00137                 logging.debug(logging_tearline % 'request parsed data end')
00138 
00139                 response = self.dispatch(method, params)
00140         except:
00141             logging.exception("An exception occurred (this is also sent as fault response to the client):")
00142             # report exception back to client
00143             response = xmlrpclib.dumps(xmlrpclib.Fault(1, self._dump_exc()))
00144         else:
00145             logging.debug(logging_tearline % 'response begin')
00146             logging.debug(response)
00147             logging.debug(logging_tearline % 'response end')
00148 
00149             if isinstance(response, xmlrpclib.Fault):
00150                 response = xmlrpclib.dumps(response)
00151             else:
00152                 # wrap response in a singleton tuple
00153                 response = (response, )
00154                 # serialize it
00155                 response = xmlrpclib.dumps(response, methodresponse=1, allow_none=True)
00156 
00157         request = request.request
00158         request.content_type = 'text/xml'
00159         request.data = response
00160         return request
00161 
00162     def dispatch(self, method, params):
00163         """ call dispatcher - for method==xxx it either locates a method called
00164             xmlrpc_xxx or loads a plugin from plugin/xmlrpc/xxx.py
00165         """
00166         method = method.replace(".", "_")
00167 
00168         try:
00169             fn = getattr(self, 'xmlrpc_' + method)
00170         except AttributeError:
00171             try:
00172                 fn = wikiutil.importPlugin(self.request.cfg, 'xmlrpc',
00173                                            method, 'execute')
00174             except wikiutil.PluginMissingError:
00175                 response = xmlrpclib.Fault(1, "No such method: %s." %
00176                                            method)
00177             else:
00178                 response = fn(self, *params)
00179         else:
00180             response = fn(*params)
00181 
00182         return response
00183 
00184     # Common faults -----------------------------------------------------
00185 
00186     def notAllowedFault(self):
00187         return xmlrpclib.Fault(1, "You are not allowed to read this page.")
00188 
00189     def noSuchPageFault(self):
00190         return xmlrpclib.Fault(1, "No such page was found.")
00191 
00192     def noLogEntryFault(self):
00193         return xmlrpclib.Fault(1, "No log entry was found.")
00194 
00195     #############################################################################
00196     ### System methods
00197     #############################################################################
00198 
00199     def xmlrpc_system_multicall(self, call_list):
00200         """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => [[4], ...]
00201 
00202         Allows the caller to package multiple XML-RPC calls into a single
00203         request.
00204 
00205         See http://www.xmlrpc.com/discuss/msgReader$1208
00206 
00207         Copied from SimpleXMLRPCServer.py
00208         """
00209 
00210         results = []
00211         for call in call_list:
00212             method_name = call['methodName']
00213             params = call['params']
00214 
00215             try:
00216                 # XXX A marshalling error in any response will fail the entire
00217                 # multicall. If someone cares they should fix this.
00218                 result = self.dispatch(method_name, params)
00219 
00220                 if not isinstance(result, xmlrpclib.Fault):
00221                     results.append([result])
00222                 else:
00223                     results.append(
00224                         {'faultCode': result.faultCode,
00225                          'faultString': result.faultString}
00226                         )
00227             except:
00228                 results.append(
00229                     {'faultCode': 1,
00230                      'faultString': "%s:%s" % (sys.exc_type, sys.exc_value)}
00231                     )
00232 
00233         return results
00234 
00235     #############################################################################
00236     ### Interface implementation
00237     #############################################################################
00238 
00239     def xmlrpc_getRPCVersionSupported(self):
00240         """ Returns version of the Wiki API.
00241 
00242         @rtype: int
00243         @return: 1 or 2 (wikirpc version)
00244         """
00245         return self.version
00246 
00247     def xmlrpc_getAllPages(self):
00248         """ Get all pages readable by current user
00249 
00250         @rtype: list
00251         @return: a list of all pages.
00252         """
00253 
00254         # the official WikiRPC interface is implemented by the extended method
00255         # as well
00256         return self.xmlrpc_getAllPagesEx()
00257 
00258 
00259     def xmlrpc_getAllPagesEx(self, opts=None):
00260         """ Get all pages readable by current user. Not an WikiRPC method.
00261 
00262         @param opts: dictionary that can contain the following arguments:
00263                 include_system:: set it to false if you do not want to see system pages
00264                 include_revno:: set it to True if you want to have lists with [pagename, revno]
00265                 include_deleted:: set it to True if you want to include deleted pages
00266                 exclude_non_writable:: do not include pages that the current user may not write to
00267                 include_underlay:: return underlay pagenames as well
00268                 prefix:: the page name must begin with this prefix to be included
00269                 mark_deleted:: returns the revision number -rev_no if the page was deleted.
00270                     Makes only sense if you enable include_revno and include_deleted.
00271         @rtype: list
00272         @return: a list of all pages.
00273         """
00274         from MoinMoin.wikisync import normalise_pagename
00275         options = {"include_system": True, "include_revno": False, "include_deleted": False,
00276                    "exclude_non_writable": False, "include_underlay": True, "prefix": "",
00277                    "pagelist": None, "mark_deleted": False}
00278         if opts is not None:
00279             options.update(opts)
00280 
00281         if not options["include_system"]:
00282             p_filter = lambda name: not wikiutil.isSystemPage(self.request, name)
00283         else:
00284             p_filter = lambda name: True
00285 
00286         if options["exclude_non_writable"]:
00287             p_filter = lambda name, p_filter=p_filter: p_filter(name) and self.request.user.may.write(name)
00288 
00289         if options["prefix"] or options["pagelist"]:
00290             def p_filter(name, p_filter=p_filter, prefix=(options["prefix"] or ""), pagelist=options["pagelist"]):
00291                 if not p_filter(name):
00292                     return False
00293                 n_name = normalise_pagename(name, prefix)
00294                 if not n_name:
00295                     return False
00296                 if not pagelist:
00297                     return True
00298                 return n_name in pagelist
00299 
00300         pagelist = self.request.rootpage.getPageList(filter=p_filter, exists=not options["include_deleted"],
00301                                                      include_underlay=options["include_underlay"],
00302                                                      return_objects=options["include_revno"])
00303 
00304         if options['include_revno']:
00305             pages = []
00306             for page in pagelist:
00307                 revno = page.get_real_rev()
00308                 if options["mark_deleted"] and not page.exists():
00309                     revno = -revno
00310                 pages.append([self._outstr(page.page_name), revno])
00311             return pages
00312         else:
00313             return [self._outstr(page) for page in pagelist]
00314 
00315     def xmlrpc_getRecentChanges(self, date):
00316         """ Get RecentChanges since date
00317 
00318         @param date: date since when rc will be listed
00319         @rtype: list
00320         @return: a list of changed pages since date, which should be in
00321             UTC. The result is a list, where each element is a struct:
00322             * name (string) :
00323                 Name of the page. The name is in UTF-8.
00324             * lastModified (date) :
00325                 Date of last modification, in UTC.
00326             * author (string) :
00327                 Name of the author (if available). UTF-8.
00328             * version (int) :
00329                 Current version.
00330         """
00331 
00332         return_items = []
00333 
00334         edit_log = editlog.EditLog(self.request)
00335         for log in edit_log.reverse():
00336             # get last-modified UTC (DateTime) from log
00337             gmtuple = tuple(time.gmtime(wikiutil.version2timestamp(log.ed_time_usecs)))
00338             lastModified_date = xmlrpclib.DateTime(gmtuple)
00339 
00340             # skip if older than "date"
00341             if lastModified_date < date:
00342                 break
00343 
00344             # skip if knowledge not permitted
00345             if not self.request.user.may.read(log.pagename):
00346                 continue
00347 
00348             # get page name (str) from log
00349             pagename_str = self._outstr(log.pagename)
00350 
00351             # get user name (str) from log
00352             author_str = log.hostname
00353             if log.userid:
00354                 userdata = user.User(self.request, log.userid)
00355                 if userdata.name:
00356                     author_str = userdata.name
00357             author_str = self._outstr(author_str)
00358 
00359             return_item = {'name': pagename_str,
00360                            'lastModified': lastModified_date,
00361                            'author': author_str,
00362                            'version': int(log.rev) }
00363             return_items.append(return_item)
00364 
00365         return return_items
00366 
00367     def xmlrpc_getPageInfo(self, pagename):
00368         """ Invoke xmlrpc_getPageInfoVersion with rev=None """
00369         return self.xmlrpc_getPageInfoVersion(pagename, rev=None)
00370 
00371     def xmlrpc_getPageInfoVersion(self, pagename, rev):
00372         """ Return page information for specific revision
00373 
00374         @param pagename: the name of the page (utf-8)
00375         @param rev: revision to get info about (int)
00376         @rtype: dict
00377         @return: page information
00378             * name (string): the canonical page name, UTF-8.
00379             * lastModified (date): Last modification date, UTC.
00380             * author (string): author name, UTF-8.
00381             * version (int): current version
00382 
00383         """
00384         pn = self._instr(pagename)
00385 
00386         # User may read this page?
00387         if not self.request.user.may.read(pn):
00388             return self.notAllowedFault()
00389 
00390         if rev is not None:
00391             page = Page(self.request, pn, rev=rev)
00392         else:
00393             page = Page(self.request, pn)
00394             rev = page.current_rev()
00395 
00396         # Non existing page?
00397         if not page.exists():
00398             return self.noSuchPageFault()
00399 
00400         # Get page info
00401         edit_info = page.edit_info()
00402         if not edit_info:
00403             return self.noLogEntryFault()
00404 
00405         mtime = wikiutil.version2timestamp(long(edit_info['timestamp'])) # must be long for py 2.2.x
00406         gmtuple = tuple(time.gmtime(mtime))
00407 
00408         version = rev # our new rev numbers: 1,2,3,4,....
00409 
00410         #######################################################################
00411         # BACKWARDS COMPATIBILITY CODE - remove when 1.2.x is regarded stone age
00412         # as we run a feed for BadContent on MoinMaster, we want to stay
00413         # compatible here for a while with 1.2.x moins asking us for BadContent
00414         # 1.3 uses the lastModified field for checking for updates, so it
00415         # should be no problem putting the old UNIX timestamp style of version
00416         # number in the version field
00417         if self.request.cfg.sitename == 'MoinMaster' and pagename == 'BadContent':
00418             version = int(mtime)
00419         #######################################################################
00420 
00421         return {
00422             'name': self._outstr(page.page_name),
00423             'lastModified': xmlrpclib.DateTime(gmtuple),
00424             'author': self._outstr(edit_info['editor']),
00425             'version': version,
00426             }
00427 
00428     def xmlrpc_getPage(self, pagename):
00429         """ Invoke xmlrpc_getPageVersion with rev=None """
00430         return self.xmlrpc_getPageVersion(pagename, rev=None)
00431 
00432     def xmlrpc_getPageVersion(self, pagename, rev):
00433         """ Get raw text from specific revision of pagename
00434 
00435         @param pagename: pagename (utf-8)
00436         @param rev: revision number (int)
00437         @rtype: str
00438         @return: utf-8 encoded page data
00439         """
00440         pagename = self._instr(pagename)
00441 
00442         # User may read page?
00443         if not self.request.user.may.read(pagename):
00444             return self.notAllowedFault()
00445 
00446         if rev is not None:
00447             page = Page(self.request, pagename, rev=rev)
00448         else:
00449             page = Page(self.request, pagename)
00450 
00451         # Non existing page?
00452         if not page.exists():
00453             return self.noSuchPageFault()
00454 
00455         # Return page raw text
00456         if self.version == 2:
00457             return self._outstr(page.get_raw_body())
00458         elif self.version == 1:
00459             return self._outlob(page.get_raw_body())
00460 
00461     def xmlrpc_getPageHTML(self, pagename):
00462         """ Invoke xmlrpc_getPageHTMLVersion with rev=None """
00463         return self.xmlrpc_getPageHTMLVersion(pagename, rev=None)
00464 
00465     def xmlrpc_getPageHTMLVersion(self, pagename, rev):
00466         """ Get HTML of from specific revision of pagename
00467 
00468         @param pagename: the page name (utf-8)
00469         @param rev: revision number (int)
00470         @rtype: str
00471         @return: page in rendered HTML (utf-8)
00472         """
00473         pagename = self._instr(pagename)
00474 
00475         # User may read page?
00476         if not self.request.user.may.read(pagename):
00477             return self.notAllowedFault()
00478 
00479         if rev is not None:
00480             page = Page(self.request, pagename, rev=rev)
00481         else:
00482             page = Page(self.request, pagename)
00483 
00484         # Non existing page?
00485         if not page.exists():
00486             return self.noSuchPageFault()
00487 
00488         # Render page into a buffer
00489         result = self.request.redirectedOutput(page.send_page, content_only=1)
00490 
00491         # Return rendered page
00492         if self.version == 2:
00493             return self._outstr(result)
00494         elif self.version == 1:
00495             return xmlrpclib.Binary(result)
00496 
00497     def xmlrpc_listLinks(self, pagename):
00498         """
00499         list links for a given page
00500         @param pagename: the page name
00501         @rtype: list
00502         @return: links of the page, structs, with the following elements
00503             * name (string) : The page name or URL the link is to, UTF-8 encoding.
00504             * type (int) : The link type. Zero (0) for internal Wiki
00505               link, one (1) for external link (URL - image link, whatever).
00506         """
00507         pagename = self._instr(pagename)
00508 
00509         # User may read page?
00510         if not self.request.user.may.read(pagename):
00511             return self.notAllowedFault()
00512 
00513         page = Page(self.request, pagename)
00514 
00515         # Non existing page?
00516         if not page.exists():
00517             return self.noSuchPageFault()
00518 
00519         links_out = []
00520         for link in page.getPageLinks(self.request):
00521             links_out.append({'name': self._outstr(link), 'type': 0 })
00522         return links_out
00523 
00524     def xmlrpc_putPage(self, pagename, pagetext):
00525         """
00526         save a page / change a page to a new text
00527         @param pagename: the page name (unicode or utf-8)
00528         @param pagetext: the new page text (content, unicode or utf-8)
00529         @rtype: bool
00530         @return: true on success
00531         """
00532 
00533         pagename = self._instr(pagename)
00534         pagename = wikiutil.normalize_pagename(pagename, self.cfg)
00535         if not pagename:
00536             return xmlrpclib.Fault("INVALID", "pagename can't be empty")
00537 
00538         # check ACLs
00539         if not self.request.user.may.write(pagename):
00540             return xmlrpclib.Fault(1, "You are not allowed to edit this page")
00541 
00542         page = PageEditor(self.request, pagename)
00543         try:
00544             if self.version == 2:
00545                 newtext = self._instr(pagetext)
00546             elif self.version == 1:
00547                 newtext = self._inlob(pagetext)
00548             msg = page.saveText(newtext, 0)
00549         except page.SaveError, msg:
00550             logging.error("SaveError: %s" % msg)
00551             return xmlrpclib.Fault(1, "%s" % msg)
00552 
00553         # Update pagelinks cache
00554         page.getPageLinks(self.request)
00555 
00556         return xmlrpclib.Boolean(1)
00557 
00558     def xmlrpc_revertPage(self, pagename, revision):
00559         """Revert a page to previous revision
00560 
00561         This is mainly intended to be used by the jabber bot.
00562 
00563         @param pagename: the page name (unicode or utf-8)
00564         @param revision: revision to revert to
00565         @rtype bool
00566         @return true on success
00567 
00568         """
00569         if not self.request.user.may.write(pagename):
00570             return xmlrpclib.Fault(1, "You are not allowed to edit this page")
00571 
00572         rev = int(self._instr(revision))
00573         editor = PageEditor(self.request, pagename)
00574 
00575         try:
00576             editor.revertPage(rev)
00577         except PageEditor.SaveError, error:
00578             return xmlrpclib.Fault(1, "Revert failed: %s" % (str(error), ))
00579 
00580         return xmlrpclib.Boolean(1)
00581 
00582     def xmlrpc_searchPages(self, query_string):
00583         """ Searches pages for query_string.
00584             Returns a list of tuples (foundpagename, context)
00585         """
00586         from MoinMoin import search
00587         results = search.searchPages(self.request, query_string)
00588         results.formatter = self.request.html_formatter
00589         results.request = self.request
00590         return [(self._outstr(hit.page_name),
00591                  self._outstr(results.formatContext(hit, 180, 1)))
00592                 for hit in results.hits]
00593 
00594     def xmlrpc_searchPagesEx(self, query_string, search_type, length, case, mtime, regexp):
00595         """ Searches pages for query_string - extended version for compatibility
00596 
00597         This function, in contrary to searchPages(), doesn't return HTML-formatted data.
00598 
00599         @param query_string: term to search for
00600         @param search_type: "text" or "title" search
00601         @param length: length of context preview (in characters)
00602         @param case: should the search be case sensitive?
00603         @param mtime: only output pages modified after mtime
00604         @param regexp: should the query_string be treates as a regular expression?
00605         @return: (page name, context preview, page url)
00606 
00607         """
00608         from MoinMoin import search
00609         from MoinMoin.formatter.text_plain import Formatter
00610 
00611         kwargs = {"sort": "page_name", "case": case, "regex": regexp}
00612         if search_type == "title":
00613             kwargs["titlesearch"] = True
00614 
00615         results = search.searchPages(self.request, query_string, **kwargs)
00616         results.formatter = Formatter(self.request)
00617         results.request = self.request
00618 
00619         return [(self._outstr(hit.page_name),
00620                  self._outstr(results.formatContext(hit, length, 1)),
00621                  self.request.getQualifiedURL(hit.page.url(self.request, {})))
00622                 for hit in results.hits]
00623 
00624     def xmlrpc_getMoinVersion(self):
00625         """ Returns a tuple of the MoinMoin version:
00626             (project, release, revision)
00627         """
00628         from MoinMoin import version
00629         return (version.project, version.release, version.revision)
00630 
00631 
00632     # user profile data transfer
00633 
00634     def xmlrpc_getUserProfile(self):
00635         """ Return the user profile data for the current user.
00636             Use this in a single multicall after applyAuthToken.
00637             If we have a valid user, returns a dict of items from user profile.
00638             Otherwise, return an empty dict.
00639         """
00640         u = self.request.user
00641         if not u.valid:
00642             userdata = {}
00643         else:
00644             userdata = dict(u.persistent_items())
00645         return userdata
00646 
00647     def xmlrpc_getUserLanguageByJID(self, jid):
00648         """ Returns user's language given his/her Jabber ID
00649 
00650         It makes no sense to consider this a secret, right? Therefore
00651         an authentication token is not required. We return a default
00652         of "en" if user is not found.
00653 
00654         TODO: surge protection? Do we fear account enumeration?
00655         """
00656         retval = "en"
00657         u = user.get_by_jabber_id(self.request, jid)
00658         if u:
00659             retval = u.language
00660 
00661         return retval
00662 
00663     # authorization methods
00664 
00665     def xmlrpc_getAuthToken(self, username, password, *args):
00666         """ Returns a token which can be used for authentication
00667             in other XMLRPC calls. If the token is empty, the username
00668             or the password were wrong.
00669 
00670             Implementation note: token is same as cookie content would be for http session
00671         """
00672         request = self.request
00673         request.session = request.cfg.session_service.get_session(request)
00674 
00675         u = auth.setup_from_session(request, request.session)
00676         u = auth.handle_login(request, u, username=username, password=password)
00677 
00678         if u and u.valid:
00679             request.user = u
00680             request.cfg.session_service.finalize(request, request.session)
00681             return request.session.sid
00682         else:
00683             return ""
00684 
00685     def xmlrpc_getJabberAuthToken(self, jid, secret):
00686         """Returns a token which can be used for authentication.
00687 
00688         This token can be used in other XMLRPC calls. Generation of
00689         token depends on user's JID and a secret shared between wiki
00690         and Jabber bot.
00691 
00692         @param jid: a bare Jabber ID
00693         """
00694         if self.cfg.secrets['jabberbot'] != secret:
00695             return ""
00696 
00697         u = self.request.handle_jid_auth(jid)
00698 
00699         if u and u.valid:
00700             return self._generate_auth_token(u)
00701         else:
00702             return ""
00703 
00704     def xmlrpc_applyAuthToken(self, auth_token):
00705         """ Applies the auth token and thereby authenticates the user. """
00706         if not auth_token:
00707             return xmlrpclib.Fault("INVALID", "Empty token.")
00708 
00709         request = self.request
00710         request.session = request.cfg.session_service.get_session(request, auth_token)
00711         u = auth.setup_from_session(request, request.session)
00712 
00713         if u and u.valid:
00714             self.request.user = u
00715             return "SUCCESS"
00716         else:
00717             return xmlrpclib.Fault("INVALID", "Invalid token.")
00718 
00719 
00720     def xmlrpc_deleteAuthToken(self, auth_token):
00721         """ Delete the given auth token. """
00722         if not auth_token:
00723             return xmlrpclib.Fault("INVALID", "Empty token.")
00724 
00725         request = self.request
00726         request.session = request.cfg.session_service.get_session(request, auth_token)
00727         request.cfg.session_service.destroy_session(request, request.session)
00728 
00729         return "SUCCESS"
00730 
00731 
00732     # methods for wiki synchronization
00733 
00734     def xmlrpc_getDiff(self, pagename, from_rev, to_rev, n_name=None):
00735         """ Gets the binary difference between two page revisions.
00736 
00737             @param pagename: unicode string qualifying the page name
00738 
00739             @param fromRev: integer specifying the source revision. May be None to
00740             refer to a virtual empty revision which leads to a diff
00741             containing the whole page.
00742 
00743             @param toRev: integer specifying the target revision. May be None to
00744             refer to the current revision. If the current revision is the same
00745             as fromRev, there will be a special error condition "ALREADY_CURRENT"
00746 
00747             @param n_name: do a tag check verifying that n_name was the normalised
00748             name of the last tag
00749 
00750             If both fromRev and toRev are None, this function acts similar to getPage, i.e. it will diff("",currentRev).
00751 
00752             @return Returns a dict:
00753             * status (not a field, implicit, returned as Fault if not SUCCESS):
00754              * "SUCCESS" - if the diff could be retrieved successfully
00755              * "NOT_EXIST" - item does not exist
00756              * "FROMREV_INVALID" - the source revision is invalid
00757              * "TOREV_INVALID" - the target revision is invalid
00758              * "INTERNAL_ERROR" - there was an internal error
00759              * "INVALID_TAG" - the last tag does not match the supplied normalised name
00760              * "ALREADY_CURRENT" - this not merely an error condition. It rather means that
00761              there is no new revision to diff against which is a good thing while
00762              synchronisation.
00763             * current: the revision number of the current revision (not the one which was diff'ed against)
00764             * diff: Binary object that transports a zlib-compressed binary diff (see bdiff.py, taken from Mercurial)
00765             * conflict: if there is a conflict on the page currently
00766 
00767         """
00768         from MoinMoin.util.bdiff import textdiff, compress
00769         from MoinMoin.wikisync import TagStore
00770 
00771         pagename = self._instr(pagename)
00772         if n_name is not None:
00773             n_name = self._instr(n_name)
00774 
00775         # User may read page?
00776         if not self.request.user.may.read(pagename):
00777             return self.notAllowedFault()
00778 
00779         def allowed_rev_type(data):
00780             if data is None:
00781                 return True
00782             return isinstance(data, int) and data > 0
00783 
00784         if not allowed_rev_type(from_rev):
00785             return xmlrpclib.Fault("FROMREV_INVALID", "Incorrect type for from_rev.")
00786 
00787         if not allowed_rev_type(to_rev):
00788             return xmlrpclib.Fault("TOREV_INVALID", "Incorrect type for to_rev.")
00789 
00790         currentpage = Page(self.request, pagename)
00791         if not currentpage.exists():
00792             return xmlrpclib.Fault("NOT_EXIST", "Page does not exist.")
00793 
00794         revisions = currentpage.getRevList()
00795 
00796         if from_rev is not None and from_rev not in revisions:
00797             return xmlrpclib.Fault("FROMREV_INVALID", "Unknown from_rev.")
00798         if to_rev is not None and to_rev not in revisions:
00799             return xmlrpclib.Fault("TOREV_INVALID", "Unknown to_rev.")
00800 
00801         # use lambda to defer execution in the next lines
00802         if from_rev is None:
00803             oldcontents = lambda: ""
00804         else:
00805             oldpage = Page(self.request, pagename, rev=from_rev)
00806             oldcontents = lambda: oldpage.get_raw_body_str()
00807 
00808         if to_rev is None:
00809             newpage = currentpage
00810             newcontents = lambda: currentpage.get_raw_body_str()
00811         else:
00812             newpage = Page(self.request, pagename, rev=to_rev)
00813             newcontents = lambda: newpage.get_raw_body_str()
00814 
00815         if oldcontents() and oldpage.get_real_rev() == newpage.get_real_rev():
00816             return xmlrpclib.Fault("ALREADY_CURRENT", "There are no changes.")
00817 
00818         if n_name is not None:
00819             tags = TagStore(newpage)
00820             last_tag = tags.get_last_tag()
00821             if last_tag is not None and last_tag.normalised_name != n_name:
00822                 return xmlrpclib.Fault("INVALID_TAG", "The used tag is incorrect because the normalised name does not match.")
00823 
00824         newcontents = newcontents()
00825         conflict = wikiutil.containsConflictMarker(newcontents)
00826         diffblob = xmlrpclib.Binary(compress(textdiff(oldcontents(), newcontents)))
00827 
00828         return {"conflict": conflict, "diff": diffblob, "diffversion": 1, "current": currentpage.get_real_rev()}
00829 
00830     def xmlrpc_interwikiName(self):
00831         """ Returns the interwiki name and the IWID of the current wiki. """
00832         name = self.request.cfg.interwikiname
00833         iwid = self.request.cfg.iwid
00834         if name is None:
00835             return [None, iwid]
00836         else:
00837             return [self._outstr(name), iwid]
00838 
00839     def xmlrpc_mergeDiff(self, pagename, diff, local_rev, delta_remote_rev, last_remote_rev, interwiki_name, normalised_name):
00840         """ Merges a diff sent by the remote machine and returns the number of the new revision.
00841             Additionally, this method tags the new revision.
00842 
00843             @param pagename: The pagename that is currently dealt with.
00844             @param diff: The diff that can be applied to the version specified by delta_remote_rev.
00845                 If it is None, the page is deleted.
00846             @param local_rev: The revno of the page on the other wiki system, used for the tag.
00847             @param delta_remote_rev: The revno that the diff is taken against.
00848             @param last_remote_rev: The last revno of the page `pagename` that is known by the other wiki site.
00849             @param interwiki_name: Used to build the interwiki tag.
00850             @param normalised_name: The normalised pagename that is common to both wikis.
00851 
00852             @return Returns the current revision number after the merge was done. Or one of the following errors:
00853                 * "SUCCESS" - the page could be merged and tagged successfully.
00854                 * "NOT_EXIST" - item does not exist and there was not any content supplied.
00855                 * "LASTREV_INVALID" - the page was changed and the revision got invalid
00856                 * "INTERNAL_ERROR" - there was an internal error
00857                 * "NOT_ALLOWED" - you are not allowed to do the merge operation on the page
00858         """
00859         from MoinMoin.util.bdiff import decompress, patch
00860         from MoinMoin.wikisync import TagStore, BOTH
00861         from MoinMoin.packages import unpackLine
00862         LASTREV_INVALID = xmlrpclib.Fault("LASTREV_INVALID", "The page was changed")
00863 
00864         pagename = self._instr(pagename)
00865 
00866         comment = u"Remote Merge - %r" % unpackLine(interwiki_name)[-1]
00867 
00868         # User may read page?
00869         if not self.request.user.may.read(pagename) or not self.request.user.may.write(pagename):
00870             return xmlrpclib.Fault("NOT_ALLOWED", "You are not allowed to write to this page.")
00871 
00872         # XXX add locking here!
00873 
00874         # current version of the page
00875         currentpage = PageEditor(self.request, pagename, do_editor_backup=0)
00876 
00877         if last_remote_rev is not None and currentpage.get_real_rev() != last_remote_rev:
00878             return LASTREV_INVALID
00879 
00880         if not currentpage.exists() and diff is None:
00881             return xmlrpclib.Fault("NOT_EXIST", "The page does not exist and no diff was supplied.")
00882 
00883         if diff is None: # delete the page
00884             try:
00885                 currentpage.deletePage(comment)
00886             except PageEditor.AccessDenied, (msg, ):
00887                 return xmlrpclib.Fault("NOT_ALLOWED", msg)
00888             return currentpage.get_real_rev()
00889 
00890         # base revision used for the diff
00891         basepage = Page(self.request, pagename, rev=(delta_remote_rev or 0))
00892 
00893         # generate the new page revision by applying the diff
00894         newcontents = patch(basepage.get_raw_body_str(), decompress(str(diff)))
00895         #print "Diff against %r" % basepage.get_raw_body_str()
00896 
00897         # write page
00898         try:
00899             currentpage.saveText(newcontents.decode("utf-8"), last_remote_rev or 0, comment=comment)
00900         except PageEditor.Unchanged: # could happen in case of both wiki's pages being equal
00901             pass
00902         except PageEditor.EditConflict:
00903             return LASTREV_INVALID
00904 
00905         current_rev = currentpage.get_real_rev()
00906 
00907         tags = TagStore(currentpage)
00908         tags.add(remote_wiki=interwiki_name, remote_rev=local_rev, current_rev=current_rev, direction=BOTH, normalised_name=normalised_name)
00909 
00910         # XXX unlock page
00911 
00912         return current_rev
00913 
00914 
00915     # XXX BEGIN WARNING XXX
00916     # All xmlrpc_*Attachment* functions have to be considered as UNSTABLE API -
00917     # they are neither standard nor are they what we need when we have switched
00918     # attachments (1.5 style) to mimetype items (hopefully in 1.6).
00919     # They will be partly removed, esp. the semantics of the function "listAttachments"
00920     # cannot be sensibly defined for items.
00921     # If the first beta or more stable release of 1.6 will have new item semantics,
00922     # we will remove the functions before it is released.
00923     def xmlrpc_listAttachments(self, pagename):
00924         """ Get all attachments associated with pagename
00925         Deprecated.
00926 
00927         @param pagename: pagename (utf-8)
00928         @rtype: list
00929         @return: a list of utf-8 attachment names
00930         """
00931         pagename = self._instr(pagename)
00932         # User may read page?
00933         if not self.request.user.may.read(pagename):
00934             return self.notAllowedFault()
00935 
00936         result = AttachFile._get_files(self.request, pagename)
00937         return result
00938 
00939     def xmlrpc_getAttachment(self, pagename, attachname):
00940         """ Get attachname associated with pagename
00941 
00942         @param pagename: pagename (utf-8)
00943         @param attachname: attachment name (utf-8)
00944         @rtype base64
00945         @return base64 data
00946         """
00947         pagename = self._instr(pagename)
00948         # User may read page?
00949         if not self.request.user.may.read(pagename):
00950             return self.notAllowedFault()
00951 
00952         filename = wikiutil.taintfilename(self._instr(attachname))
00953         filename = AttachFile.getFilename(self.request, pagename, filename)
00954         if not os.path.isfile(filename):
00955             return self.noSuchPageFault()
00956         return self._outlob(open(filename, 'rb').read())
00957 
00958     def xmlrpc_putAttachment(self, pagename, attachname, data):
00959         """ Set attachname associated with pagename to data
00960 
00961         @param pagename: pagename (utf-8)
00962         @param attachname: attachment name (utf-8)
00963         @param data: file data (base64)
00964         @rtype boolean
00965         @return True if attachment was set
00966         """
00967         pagename = self._instr(pagename)
00968         # User may read page?
00969         if not self.request.user.may.read(pagename):
00970             return self.notAllowedFault()
00971 
00972         # also check ACLs
00973         if not self.request.user.may.write(pagename):
00974             return xmlrpclib.Fault(1, "You are not allowed to edit this page")
00975 
00976         attachname = wikiutil.taintfilename(attachname)
00977         filename = AttachFile.getFilename(self.request, pagename, attachname)
00978         if os.path.exists(filename) and not os.path.isfile(filename):
00979             return self.noSuchPageFault()
00980         open(filename, 'wb+').write(data.data)
00981         AttachFile._addLogEntry(self.request, 'ATTNEW', pagename, filename)
00982         return xmlrpclib.Boolean(1)
00983 
00984     # XXX END WARNING XXX
00985 
00986 
00987     def xmlrpc_getBotTranslations(self):
00988         """ Return translations to be used by notification bot
00989 
00990         @return: a dict (indexed by language) of dicts of translated strings (indexed by original ones)
00991         """
00992         from MoinMoin.i18n import bot_translations
00993         return bot_translations(self.request)
00994 
00995 
00996 class XmlRpc1(XmlRpcBase):
00997 
00998     def __init__(self, request):
00999         XmlRpcBase.__init__(self, request)
01000         self.version = 1
01001 
01002     def _instr(self, text):
01003         """ Convert string we get from xmlrpc into internal representation
01004 
01005         @param text: quoted text (str or unicode object)
01006         @rtype: unicode
01007         @return: text
01008         """
01009         return wikiutil.url_unquote(text) # config.charset must be utf-8
01010 
01011     def _outstr(self, text):
01012         """ Convert string from internal representation to xmlrpc
01013 
01014         @param text: unicode or string in config.charset
01015         @rtype: str
01016         @return: text encoded in utf-8 and quoted
01017         """
01018         return wikiutil.url_quote(text) # config.charset must be utf-8
01019 
01020 
01021 class XmlRpc2(XmlRpcBase):
01022 
01023     def __init__(self, request):
01024         XmlRpcBase.__init__(self, request)
01025         self.version = 2
01026 
01027     def _instr(self, text):
01028         """ Convert string we get from xmlrpc into internal representation
01029 
01030         @param text: unicode or utf-8 string
01031         @rtype: unicode
01032         @return: text
01033         """
01034         if not isinstance(text, unicode):
01035             text = unicode(text, 'utf-8')
01036         return text
01037 
01038     def _outstr(self, text):
01039         """ Convert string from internal representation to xmlrpc
01040 
01041         @param text: unicode or string in config.charset
01042         @rtype: str
01043         @return: text encoded in utf-8
01044         """
01045         if isinstance(text, unicode):
01046             text = text.encode('utf-8')
01047         elif config.charset != 'utf-8':
01048             text = unicode(text, config.charset).encode('utf-8')
01049         return text
01050 
01051 
01052 def xmlrpc(request):
01053     return XmlRpc1(request).process()
01054 
01055 def xmlrpc2(request):
01056     return XmlRpc2(request).process()
01057