view MoinMoin/wikirpc.py @ 622:8fba6d15a2f5

make request.(html_)formatter available early and use it
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 06 May 2006 00:09:06 +0200
parents 2202f548cbb0
children
line wrap: on
line source
# -*- coding: iso-8859-1 -*-
"""
    MoinMoin - Wiki XMLRPC v1 and v2 Interface

    If you want to use wikirpc function "putPage", read the comments in
    xmlrpc_putPage or it won't work!
    
    Parts of this code are based on Jürgen Hermann's wikirpc.py,
    Les Orchard's "xmlrpc.cgi" and further work by Gustavo Niemeyer.

    See http://www.ecyrd.com/JSPWiki/Wiki.jsp?page=WikiRPCInterface
    and http://www.decafbad.com/twiki/bin/view/Main/XmlRpcToWiki
    for specs on many of the functions here.

    See also http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2
    for the new stuff.

    The main difference between v1 and v2 is that v2 relies on utf-8
    as transport encoding. No url-encoding and no base64 anymore, except
    when really necessary (like for transferring binary files like
    attachments maybe).

    @copyright: 2003-2005 by Thomas Waldmann
    @license: GNU GPL, see COPYING for details
"""

import sys, time, xmlrpclib

from MoinMoin import config, user, wikiutil
from MoinMoin.Page import Page
from MoinMoin.PageEditor import PageEditor
from MoinMoin.logfile import editlog

_debug = 0

class XmlRpcBase:
    def __init__(self, request):
        """
        Initialize an XmlRpcBase object.
        @param request: the request object
        """
        self.request = request
        self.version = None # this has to be defined in derived class
        self.cfg = request.cfg

    #############################################################################
    ### Helper functions           
    #############################################################################

    def _instr(self, text):
        """ Convert inbound string from utf-8.
        
        @param text: the text to convert
        @rtype: str
        @return: string in config.charset
        """
        raise "NotImplementedError"
    
    def _outstr(self, text):
        """ Convert outbound string to utf-8.

        @param text: the text to convert XXX unicode? str? both?
        @rtype: str
        @return: string in utf-8
        """
        raise "NotImplementedError"
    
    def _inlob(self, text):
        """ Convert inbound base64-encoded utf-8 to Large OBject.
        
        @param text: the text to convert
        @rtype: unicode
        @return: text
        """
        text = text.data #this is a already base64-decoded 8bit string
        text = unicode(text, 'utf-8')
        return text

    def _outlob(self, text):
        """ Convert outbound Large OBject to base64-encoded utf-8.
        
        @param text: the text, either unicode or utf-8 string
        @rtype: str
        @return: xmlrpc Binary object
        """
        if isinstance(text, unicode):
            text = text.encode('utf-8')
        else:
            if config.charset != 'utf-8':
                text = unicode(text, config.charset).encode('utf-8')
        return xmlrpclib.Binary(text)
                    
    def _dump_exc(self):
        """ Convert an exception to a string.
        
        @rtype: str
        @return: traceback as string
        """
        import traceback

        return "%s: %s\n%s" % (
            sys.exc_info()[0],
            sys.exc_info()[1],
            '\n'.join(traceback.format_tb(sys.exc_info()[2])),
        )


    #############################################################################
    ### Interface implementation
    #############################################################################

    def xmlrpc_getRPCVersionSupported(self):
        """ Returns version of the Wiki API.

        @rtype: int
        @return: 1 or 2 (wikirpc version)
        """
        return self.version

    def xmlrpc_getAllPages(self):
        """ Get all pages readable by current user

        @rtype: list
        @return: a list of all pages. The result is a list of utf-8 strings.
        """
        pagelist = self.request.rootpage.getPageList()
        return map(self._outstr, pagelist)

    def xmlrpc_getRecentChanges(self, date):
        """ Get RecentChanges since date
        
        @param date: date since when rc will be listed
        @rtype: list
        @return: a list of changed pages since date, which should be in
            UTC. The result is a list, where each element is a struct:
            * name (string) :
                Name of the page. The name is in UTF-8.
            * lastModified (date) :
                Date of last modification, in UTC.
            * author (string) :
                Name of the author (if available). UTF-8.
            * version (int) :
                Current version.
        """
        
        return_items = []
        
        edit_log = editlog.EditLog(self.request)
        for log in edit_log.reverse():
            # get last-modified UTC (DateTime) from log
            gmtuple = tuple(time.gmtime(wikiutil.version2timestamp(log.ed_time_usecs)))
            lastModified_date = xmlrpclib.DateTime(gmtuple)

            # skip if older than "date"
            if lastModified_date < date:
                break
            
            # skip if knowledge not permitted
            if not self.request.user.may.read(log.pagename):
                continue
            
            # get page name (str) from log
            pagename_str = self._outstr(log.pagename)

            # get user name (str) from log
            author_str = log.hostname
            if log.userid:
                userdata = user.User(self.request, log.userid)
                if userdata.name:
                    author_str = userdata.name
            author_str = self._outstr(author_str)

            return_item = { 'name':  pagename_str,
                            'lastModified': lastModified_date,
                            'author': author_str,
                            'version': int(log.rev) }
            return_items.append(return_item)
        
        return return_items

    def xmlrpc_getPageInfo(self, pagename):
        """ Invoke xmlrpc_getPageInfoVersion with rev=None """
        return self.xmlrpc_getPageInfoVersion(pagename, rev=None)

    def xmlrpc_getPageInfoVersion(self, pagename, rev):
        """ Return page information for specific revision
        
        @param pagename: the name of the page (utf-8)
        @param rev: revision to get info about (XXX int?)
        @rtype: dict XXX ??
        @return: page information
            * name (string): the canonical page name, UTF-8.
            * lastModified (date): Last modification date, UTC.
            * author (string): author name, UTF-8.
            * version (int): current version

        """
        pn = self._instr(pagename)

        # User may read this page?
        if not self.request.user.may.read(pn):
            return self.notAllowedFault()

        if rev != None:
            page = Page(self.request, pn, rev=rev)
        else:
            page = Page(self.request, pn)
            rev = page.current_rev()

        # Non existing page?
        if not page.exists():
            return self.noSuchPageFault()

        # Get page info
        last_edit = page.last_edit(self.request)           
        mtime = wikiutil.version2timestamp(long(last_edit['timestamp'])) # must be long for py 2.2.x
        gmtuple = tuple(time.gmtime(mtime))
        
        version = rev # our new rev numbers: 1,2,3,4,....

        #######################################################################
        # BACKWARDS COMPATIBILITY CODE - remove when 1.2.x is regarded stone age
        # as we run a feed for BadContent on MoinMaster, we want to stay
        # compatible here for a while with 1.2.x moins asking us for BadContent
        # 1.3 uses the lastModified field for checking for updates, so it
        # should be no problem putting the old UNIX timestamp style of version
        # number in the version field
        if self.request.cfg.sitename == 'MoinMaster' and pagename == 'BadContent':
            version = int(mtime)
        #######################################################################
            
        return {
            'name': self._outstr(page.page_name),
            'lastModified' : xmlrpclib.DateTime(gmtuple),
            'author': self._outstr(last_edit['editor']),
            'version': version,
            }

    def xmlrpc_getPage(self, pagename):
        """ Invoke xmlrpc_getPageVersion with rev=None """
        return self.xmlrpc_getPageVersion(pagename, rev=None)

    def xmlrpc_getPageVersion(self, pagename, rev):
        """ Get raw text from specific revision of pagename
        
        @param pagename: pagename (utf-8)
        @param rev: revision number (int)
        @rtype: str
        @return: utf-8 encoded page data
        """    
        pagename = self._instr(pagename)

        # User may read page?
        if not self.request.user.may.read(pagename):
            return self.notAllowedFault()

        if rev != None:
            page = Page(self.request, pagename, rev=rev)
        else:
            page = Page(self.request, pagename)

        # Non existing page?
        if not page.exists():
            return self.noSuchPageFault()

        # Return page raw text
        if self.version == 2:
            return self._outstr(page.get_raw_body())
        elif self.version == 1:
            return self._outlob(page.get_raw_body())

    def xmlrpc_getPageHTML(self, pagename):
        """ Invoke xmlrpc_getPageHTMLVersion with rev=None """
        return self.xmlrpc_getPageHTMLVersion(pagename, rev=None)

    def xmlrpc_getPageHTMLVersion(self, pagename, rev):
        """ Get HTML of from specific revision of pagename
        
        @param pagename: the page name (utf-8)
        @param rev: revision number (int)
        @rtype: str
        @return: page in rendered HTML (utf-8)
        """
        pagename = self._instr(pagename)

        # User may read page?
        if not self.request.user.may.read(pagename):
            return self.notAllowedFault()

        if rev != None:
            page = Page(self.request, pagename, rev=rev)
        else:
            page = Page(self.request, pagename)

        # Non existing page?
        if not page.exists():
            return self.noSuchPageFault()
        
        # Render page into a buffer
        result = self.request.redirectedOutput(page.send_page, self.request,
                                               content_only=1)
        
        # Return rendered page
        if self.version == 2:
            return self._outstr(result)
        elif self.version == 1:
            return xmlrpclib.Binary(result)

    def xmlrpc_listLinks(self, pagename):
        """
        list links for a given page
        @param pagename: the page name
        @rtype: list
        @return: links of the page, structs, with the following elements
            * name (string) : The page name or URL the link is to, UTF-8 encoding.
            * type (int) : The link type. Zero (0) for internal Wiki
              link, one (1) for external link (URL - image link, whatever).
        """
        pagename = self._instr(pagename)

        # User may read page?
        if not self.request.user.may.read(pagename):
            return self.notAllowedFault()

        page = Page(self.request, pagename)

        # Non existing page?
        if not page.exists():
            return self.noSuchPageFault()
        
        links_out = []
        for link in page.getPageLinks(self.request):
            links_out.append({ 'name': self._outstr(link), 'type': 0 })
        return links_out

    def xmlrpc_putPage(self, pagename, pagetext):
        """
        save a page / change a page to a new text
        @param pagename: the page name (unicode or utf-8)
        @param pagetext: the new page text (content, unicode or utf-8)
        @rtype: bool
        @return: true on success
        """
        # READ THIS OR IT WILL NOT WORK ===================================
        
        # we use a test page instead of using the requested pagename, if
        # xmlrpc_putpage_enabled was not set in wikiconfig.
        
        if self.request.cfg.xmlrpc_putpage_enabled:
            pagename = self._instr(pagename)
        else:
            pagename = u"PutPageTestPage"

        # By default, only authenticated (trusted) users may use putPage!
        # Trusted currently means being authenticated by http auth.
        # if you also want untrusted users to be able to write pages, then
        # change your wikiconfig to have xmlrpc_putpage_trusted_only = 0
        # and make very very sure that nobody untrusted can access your wiki
        # via network or somebody will raid your wiki some day!
        
        if self.request.cfg.xmlrpc_putpage_trusted_only and not self.request.user.trusted:
            return xmlrpclib.Fault(1, "You are not allowed to edit this page")

        # also check ACLs
        if not self.request.user.may.write(pagename):
            return xmlrpclib.Fault(1, "You are not allowed to edit this page")

        # =================================================================

        page = PageEditor(self.request, pagename)
        try:
            if self.version == 2:
                newtext = self._instr(pagetext)
            elif self.version == 1:
                newtext = self._inlob(pagetext)
            msg = page.saveText(newtext, 0)
        except page.SaveError, msg:
            pass
        if _debug and msg:
            sys.stderr.write("Msg: %s\n" % msg)

        # Update pagelinks cache
        page.getPageLinks(self.request)

        return xmlrpclib.Boolean(1)

    def xmlrpc_searchPages(self, query_string):
        from MoinMoin import search
        query = search.QueryParser().parse_query(query_string)
        results = search.searchPages(self.request, query)
        results.formatter = self.request.html_formatter
        results.request = self.request
        return [(self._outstr(hit.page_name),
                 self._outstr(results.formatContext(hit, 180, 1)))
                for hit in results.hits]

    def process(self):
        """ xmlrpc v1 and v2 dispatcher """
        try:
            data = self.request.read()
            params, method = xmlrpclib.loads(data)
    
            if _debug:
                sys.stderr.write('- XMLRPC ' + '-' * 70 + '\n')
                sys.stderr.write('%s(%s)\n\n' % (method, repr(params)))
            
            try:
                fn = getattr(self, 'xmlrpc_' + method)
            except AttributeError:
                try:
                    fn = wikiutil.importPlugin(self.request.cfg, 'xmlrpc',
                                               method, 'execute')
                except wikiutil.PluginMissingError:
                    response = xmlrpclib.Fault(1, "No such method: %s." %
                                               method)
                else:
                    response = fn(self, *params)
            else:
                response = fn(*params)
        except:
            # report exception back to server
            response = xmlrpclib.dumps(xmlrpclib.Fault(1, self._dump_exc()))
        else:
            # wrap response in a singleton tuple
            response = (response,)

            # serialize it
            response = xmlrpclib.dumps(response, methodresponse=1)

        self.request.http_headers([
            "Content-Type: text/xml;charset=utf-8",
            "Content-Length: %d" % len(response),
        ])
        self.request.write(response)

        if _debug:
            sys.stderr.write('- XMLRPC ' + '-' * 70 + '\n')
            sys.stderr.write(response + '\n\n')

    # Common faults -----------------------------------------------------
    
    def notAllowedFault(self):
        return xmlrpclib.Fault(1, "You are not allowed to read this page.")

    def noSuchPageFault(self):
        return xmlrpclib.Fault(1, "No such page was found.")        


class XmlRpc1(XmlRpcBase):
    
    def __init__(self, request):
        XmlRpcBase.__init__(self, request)
        self.version = 1

    def _instr(self, text):
        """ Convert string we get from xmlrpc into internal representation

        @param text: quoted text (str or unicode object)
        @rtype: unicode
        @return: text
        """
        return wikiutil.url_unquote(text) # config.charset must be utf-8

    def _outstr(self, text):
        """ Convert string from internal representation to xmlrpc

        @param text: unicode or string in config.charset
        @rtype: str
        @return: text encoded in utf-8 and quoted
        """
        return wikiutil.url_quote(text) # config.charset must be utf-8

    
class XmlRpc2(XmlRpcBase):
    
    def __init__(self, request):
        XmlRpcBase.__init__(self, request)
        self.version = 2

    def _instr(self, text):
        """ Convert string we get from xmlrpc into internal representation

        @param text: unicode or utf-8 string
        @rtype: unicode
        @return: text
        """
        if not isinstance(text, unicode):
            text = unicode(text, 'utf-8')
        return text

    def _outstr(self, text):
        """ Convert string from internal representation to xmlrpc

        @param text: unicode or string in config.charset
        @rtype: str
        @return: text encoded in utf-8
        """
        if isinstance(text, unicode):
            text = text.encode('utf-8')           
        elif config.charset != 'utf-8':        
            text = unicode(text, config.charset).encode('utf-8')               
        return text


def xmlrpc(request):
    XmlRpc1(request).process()


def xmlrpc2(request):
    XmlRpc2(request).process()