~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/xmlrpc/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- coding: iso-8859-1 -*-
2
2
"""
3
 
    MoinMoin - xmlrpc extension modules Package
4
 
 
5
 
    @copyright: 2003-2004 by Thomas Waldmann
6
 
    @license: GNU GPL, see COPYING for details.
 
3
    MoinMoin - Wiki XMLRPC v1 and v2 Interface + plugin extensions
 
4
 
 
5
    Parts of this code are based on Juergen Hermann's wikirpc.py,
 
6
    Les Orchard's "xmlrpc.cgi" and further work by Gustavo Niemeyer.
 
7
 
 
8
    See http://www.ecyrd.com/JSPWiki/Wiki.jsp?page=WikiRPCInterface
 
9
    and http://www.decafbad.com/twiki/bin/view/Main/XmlRpcToWiki
 
10
    for specs on many of the functions here.
 
11
 
 
12
    See also http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2
 
13
    for the new stuff.
 
14
 
 
15
    The main difference between v1 and v2 is that v2 relies on utf-8
 
16
    as transport encoding. No url-encoding and no base64 anymore, except
 
17
    when really necessary (like for transferring binary files like
 
18
    attachments maybe).
 
19
 
 
20
    @copyright: 2003-2008 MoinMoin:ThomasWaldmann,
 
21
                2004-2006 MoinMoin:AlexanderSchremmer
 
22
    @license: GNU GPL, see COPYING for details
7
23
"""
8
24
from MoinMoin.util import pysupport
9
25
 
10
26
modules = pysupport.getPackageModules(__file__)
 
27
 
 
28
import os, sys, time, xmlrpclib
 
29
 
 
30
from MoinMoin import log
 
31
logging = log.getLogger(__name__)
 
32
 
 
33
from MoinMoin import config, user, wikiutil
 
34
from MoinMoin.Page import Page
 
35
from MoinMoin.PageEditor import PageEditor
 
36
from MoinMoin.logfile import editlog
 
37
from MoinMoin.action import AttachFile
 
38
from MoinMoin import caching
 
39
from MoinMoin import session
 
40
 
 
41
 
 
42
class XmlRpcAuthTokenIDHandler(session.SessionIDHandler):
 
43
    def __init__(self, token=None):
 
44
        session.SessionIDHandler.__init__(self)
 
45
        self.token = token
 
46
 
 
47
    def get(self, request):
 
48
        return self.token
 
49
 
 
50
    def set(self, request, session_id, expires):
 
51
        self.token = session_id
 
52
 
 
53
 
 
54
logging_tearline = '- XMLRPC %s ' + '-' * 40
 
55
 
 
56
class XmlRpcBase:
 
57
    """ XMLRPC base class with common functionality of wiki xmlrpc v1 and v2 """
 
58
    def __init__(self, request):
 
59
        """
 
60
        Initialize an XmlRpcBase object.
 
61
        @param request: the request object
 
62
        """
 
63
        self.request = request
 
64
        self.version = None # this has to be defined in derived class
 
65
        self.cfg = request.cfg
 
66
 
 
67
    #############################################################################
 
68
    ### Helper functions
 
69
    #############################################################################
 
70
 
 
71
    def _instr(self, text):
 
72
        """ Convert inbound string.
 
73
 
 
74
        @param text: the text to convert (encoded str or unicode)
 
75
        @rtype: unicode
 
76
        @return: text as unicode
 
77
        """
 
78
        raise NotImplementedError("please implement _instr in derived class")
 
79
 
 
80
    def _outstr(self, text):
 
81
        """ Convert outbound string.
 
82
 
 
83
        @param text: the text to convert (encoded str or unicode)
 
84
        @rtype: str
 
85
        @return: text as encoded str
 
86
        """
 
87
        raise NotImplementedError("please implement _outstr in derived class")
 
88
 
 
89
    def _inlob(self, text):
 
90
        """ Convert inbound base64-encoded utf-8 to Large OBject.
 
91
 
 
92
        @param text: the text to convert
 
93
        @rtype: unicode
 
94
        @return: text
 
95
        """
 
96
        text = text.data #this is a already base64-decoded 8bit string
 
97
        text = unicode(text, 'utf-8')
 
98
        return text
 
99
 
 
100
    def _outlob(self, text):
 
101
        """ Convert outbound Large OBject to base64-encoded utf-8.
 
102
 
 
103
        @param text: the text, either unicode or utf-8 string
 
104
        @rtype: str
 
105
        @return: xmlrpc Binary object
 
106
        """
 
107
        if isinstance(text, unicode):
 
108
            text = text.encode('utf-8')
 
109
        else:
 
110
            if config.charset != 'utf-8':
 
111
                text = unicode(text, config.charset).encode('utf-8')
 
112
        return xmlrpclib.Binary(text)
 
113
 
 
114
    def _dump_exc(self):
 
115
        """ Convert an exception to a string.
 
116
 
 
117
        @rtype: str
 
118
        @return: traceback as string
 
119
        """
 
120
        import traceback
 
121
 
 
122
        return "%s: %s\n%s" % (
 
123
            sys.exc_info()[0],
 
124
            sys.exc_info()[1],
 
125
            '\n'.join(traceback.format_tb(sys.exc_info()[2])),
 
126
        )
 
127
 
 
128
    def process(self):
 
129
        """ xmlrpc v1 and v2 dispatcher """
 
130
        try:
 
131
            if 'xmlrpc' in self.request.cfg.actions_excluded:
 
132
                # we do not handle xmlrpc v1 and v2 differently
 
133
                response = xmlrpclib.Fault(1, "This moin wiki does not allow xmlrpc method calls.")
 
134
            else:
 
135
                data = self.request.read(self.request.content_length)
 
136
 
 
137
                try:
 
138
                    params, method = xmlrpclib.loads(data)
 
139
                except:
 
140
                    # if anything goes wrong here, we want to see the raw data:
 
141
                    logging.debug("Length of raw data: %d bytes" % len(data))
 
142
                    logging.debug(logging_tearline % 'request raw data begin')
 
143
                    logging.debug('%r' % data)
 
144
                    logging.debug(logging_tearline % 'request raw data end')
 
145
                    raise
 
146
 
 
147
                logging.debug(logging_tearline % 'request parsed data begin')
 
148
                logging.debug('%s(%r)' % (method, params))
 
149
                logging.debug(logging_tearline % 'request parsed data end')
 
150
 
 
151
                response = self.dispatch(method, params)
 
152
        except:
 
153
            logging.exception("An exception occurred (this is also sent as fault response to the client):")
 
154
            # report exception back to client
 
155
            response = xmlrpclib.dumps(xmlrpclib.Fault(1, self._dump_exc()))
 
156
        else:
 
157
            logging.debug(logging_tearline % 'response begin')
 
158
            logging.debug(response)
 
159
            logging.debug(logging_tearline % 'response end')
 
160
 
 
161
            if isinstance(response, xmlrpclib.Fault):
 
162
                response = xmlrpclib.dumps(response)
 
163
            else:
 
164
                # wrap response in a singleton tuple
 
165
                response = (response, )
 
166
                # serialize it
 
167
                response = xmlrpclib.dumps(response, methodresponse=1, allow_none=True)
 
168
 
 
169
        self.request.emit_http_headers([
 
170
            "Content-Type: text/xml; charset=utf-8",
 
171
            "Content-Length: %d" % len(response),
 
172
        ])
 
173
        self.request.write(response)
 
174
 
 
175
    def dispatch(self, method, params):
 
176
        """ call dispatcher - for method==xxx it either locates a method called
 
177
            xmlrpc_xxx or loads a plugin from plugin/xmlrpc/xxx.py
 
178
        """
 
179
        method = method.replace(".", "_")
 
180
 
 
181
        try:
 
182
            fn = getattr(self, 'xmlrpc_' + method)
 
183
        except AttributeError:
 
184
            try:
 
185
                fn = wikiutil.importPlugin(self.request.cfg, 'xmlrpc',
 
186
                                           method, 'execute')
 
187
            except wikiutil.PluginMissingError:
 
188
                response = xmlrpclib.Fault(1, "No such method: %s." %
 
189
                                           method)
 
190
            else:
 
191
                response = fn(self, *params)
 
192
        else:
 
193
            response = fn(*params)
 
194
 
 
195
        return response
 
196
 
 
197
    # Common faults -----------------------------------------------------
 
198
 
 
199
    def notAllowedFault(self):
 
200
        return xmlrpclib.Fault(1, "You are not allowed to read this page.")
 
201
 
 
202
    def noSuchPageFault(self):
 
203
        return xmlrpclib.Fault(1, "No such page was found.")
 
204
 
 
205
    def noLogEntryFault(self):
 
206
        return xmlrpclib.Fault(1, "No log entry was found.")
 
207
 
 
208
    #############################################################################
 
209
    ### System methods
 
210
    #############################################################################
 
211
 
 
212
    def xmlrpc_system_multicall(self, call_list):
 
213
        """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => [[4], ...]
 
214
 
 
215
        Allows the caller to package multiple XML-RPC calls into a single
 
216
        request.
 
217
 
 
218
        See http://www.xmlrpc.com/discuss/msgReader$1208
 
219
 
 
220
        Copied from SimpleXMLRPCServer.py
 
221
        """
 
222
 
 
223
        results = []
 
224
        for call in call_list:
 
225
            method_name = call['methodName']
 
226
            params = call['params']
 
227
 
 
228
            try:
 
229
                # XXX A marshalling error in any response will fail the entire
 
230
                # multicall. If someone cares they should fix this.
 
231
                result = self.dispatch(method_name, params)
 
232
 
 
233
                if not isinstance(result, xmlrpclib.Fault):
 
234
                    results.append([result])
 
235
                else:
 
236
                    results.append(
 
237
                        {'faultCode': result.faultCode,
 
238
                         'faultString': result.faultString}
 
239
                        )
 
240
            except:
 
241
                results.append(
 
242
                    {'faultCode': 1,
 
243
                     'faultString': "%s:%s" % (sys.exc_type, sys.exc_value)}
 
244
                    )
 
245
 
 
246
        return results
 
247
 
 
248
    #############################################################################
 
249
    ### Interface implementation
 
250
    #############################################################################
 
251
 
 
252
    def xmlrpc_getRPCVersionSupported(self):
 
253
        """ Returns version of the Wiki API.
 
254
 
 
255
        @rtype: int
 
256
        @return: 1 or 2 (wikirpc version)
 
257
        """
 
258
        return self.version
 
259
 
 
260
    def xmlrpc_getAllPages(self):
 
261
        """ Get all pages readable by current user
 
262
 
 
263
        @rtype: list
 
264
        @return: a list of all pages.
 
265
        """
 
266
 
 
267
        # the official WikiRPC interface is implemented by the extended method
 
268
        # as well
 
269
        return self.xmlrpc_getAllPagesEx()
 
270
 
 
271
 
 
272
    def xmlrpc_getAllPagesEx(self, opts=None):
 
273
        """ Get all pages readable by current user. Not an WikiRPC method.
 
274
 
 
275
        @param opts: dictionary that can contain the following arguments:
 
276
                include_system:: set it to false if you do not want to see system pages
 
277
                include_revno:: set it to True if you want to have lists with [pagename, revno]
 
278
                include_deleted:: set it to True if you want to include deleted pages
 
279
                exclude_non_writable:: do not include pages that the current user may not write to
 
280
                include_underlay:: return underlay pagenames as well
 
281
                prefix:: the page name must begin with this prefix to be included
 
282
                mark_deleted:: returns the revision number -rev_no if the page was deleted.
 
283
                    Makes only sense if you enable include_revno and include_deleted.
 
284
        @rtype: list
 
285
        @return: a list of all pages.
 
286
        """
 
287
        from MoinMoin.wikisync import normalise_pagename
 
288
        options = {"include_system": True, "include_revno": False, "include_deleted": False,
 
289
                   "exclude_non_writable": False, "include_underlay": True, "prefix": "",
 
290
                   "pagelist": None, "mark_deleted": False}
 
291
        if opts is not None:
 
292
            options.update(opts)
 
293
 
 
294
        if not options["include_system"]:
 
295
            p_filter = lambda name: not wikiutil.isSystemPage(self.request, name)
 
296
        else:
 
297
            p_filter = lambda name: True
 
298
 
 
299
        if options["exclude_non_writable"]:
 
300
            p_filter = lambda name, p_filter=p_filter: p_filter(name) and self.request.user.may.write(name)
 
301
 
 
302
        if options["prefix"] or options["pagelist"]:
 
303
            def p_filter(name, p_filter=p_filter, prefix=(options["prefix"] or ""), pagelist=options["pagelist"]):
 
304
                if not p_filter(name):
 
305
                    return False
 
306
                n_name = normalise_pagename(name, prefix)
 
307
                if not n_name:
 
308
                    return False
 
309
                if not pagelist:
 
310
                    return True
 
311
                return n_name in pagelist
 
312
 
 
313
        pagelist = self.request.rootpage.getPageList(filter=p_filter, exists=not options["include_deleted"],
 
314
                                                     include_underlay=options["include_underlay"],
 
315
                                                     return_objects=options["include_revno"])
 
316
 
 
317
        if options['include_revno']:
 
318
            pages = []
 
319
            for page in pagelist:
 
320
                revno = page.get_real_rev()
 
321
                if options["mark_deleted"] and not page.exists():
 
322
                    revno = -revno
 
323
                pages.append([self._outstr(page.page_name), revno])
 
324
            return pages
 
325
        else:
 
326
            return [self._outstr(page) for page in pagelist]
 
327
 
 
328
    def xmlrpc_getRecentChanges(self, date):
 
329
        """ Get RecentChanges since date
 
330
 
 
331
        @param date: date since when rc will be listed
 
332
        @rtype: list
 
333
        @return: a list of changed pages since date, which should be in
 
334
            UTC. The result is a list, where each element is a struct:
 
335
            * name (string) :
 
336
                Name of the page. The name is in UTF-8.
 
337
            * lastModified (date) :
 
338
                Date of last modification, in UTC.
 
339
            * author (string) :
 
340
                Name of the author (if available). UTF-8.
 
341
            * version (int) :
 
342
                Current version.
 
343
        """
 
344
 
 
345
        return_items = []
 
346
 
 
347
        edit_log = editlog.EditLog(self.request)
 
348
        for log in edit_log.reverse():
 
349
            # get last-modified UTC (DateTime) from log
 
350
            gmtuple = tuple(time.gmtime(wikiutil.version2timestamp(log.ed_time_usecs)))
 
351
            lastModified_date = xmlrpclib.DateTime(gmtuple)
 
352
 
 
353
            # skip if older than "date"
 
354
            if lastModified_date < date:
 
355
                break
 
356
 
 
357
            # skip if knowledge not permitted
 
358
            if not self.request.user.may.read(log.pagename):
 
359
                continue
 
360
 
 
361
            # get page name (str) from log
 
362
            pagename_str = self._outstr(log.pagename)
 
363
 
 
364
            # get user name (str) from log
 
365
            author_str = log.hostname
 
366
            if log.userid:
 
367
                userdata = user.User(self.request, log.userid)
 
368
                if userdata.name:
 
369
                    author_str = userdata.name
 
370
            author_str = self._outstr(author_str)
 
371
 
 
372
            return_item = {'name': pagename_str,
 
373
                           'lastModified': lastModified_date,
 
374
                           'author': author_str,
 
375
                           'version': int(log.rev) }
 
376
            return_items.append(return_item)
 
377
 
 
378
        return return_items
 
379
 
 
380
    def xmlrpc_getPageInfo(self, pagename):
 
381
        """ Invoke xmlrpc_getPageInfoVersion with rev=None """
 
382
        return self.xmlrpc_getPageInfoVersion(pagename, rev=None)
 
383
 
 
384
    def xmlrpc_getPageInfoVersion(self, pagename, rev):
 
385
        """ Return page information for specific revision
 
386
 
 
387
        @param pagename: the name of the page (utf-8)
 
388
        @param rev: revision to get info about (int)
 
389
        @rtype: dict
 
390
        @return: page information
 
391
            * name (string): the canonical page name, UTF-8.
 
392
            * lastModified (date): Last modification date, UTC.
 
393
            * author (string): author name, UTF-8.
 
394
            * version (int): current version
 
395
 
 
396
        """
 
397
        pn = self._instr(pagename)
 
398
 
 
399
        # User may read this page?
 
400
        if not self.request.user.may.read(pn):
 
401
            return self.notAllowedFault()
 
402
 
 
403
        if rev is not None:
 
404
            page = Page(self.request, pn, rev=rev)
 
405
        else:
 
406
            page = Page(self.request, pn)
 
407
            rev = page.current_rev()
 
408
 
 
409
        # Non existing page?
 
410
        if not page.exists():
 
411
            return self.noSuchPageFault()
 
412
 
 
413
        # Get page info
 
414
        edit_info = page.edit_info()
 
415
        if not edit_info:
 
416
            return self.noLogEntryFault()
 
417
 
 
418
        mtime = wikiutil.version2timestamp(long(edit_info['timestamp'])) # must be long for py 2.2.x
 
419
        gmtuple = tuple(time.gmtime(mtime))
 
420
 
 
421
        version = rev # our new rev numbers: 1,2,3,4,....
 
422
 
 
423
        #######################################################################
 
424
        # BACKWARDS COMPATIBILITY CODE - remove when 1.2.x is regarded stone age
 
425
        # as we run a feed for BadContent on MoinMaster, we want to stay
 
426
        # compatible here for a while with 1.2.x moins asking us for BadContent
 
427
        # 1.3 uses the lastModified field for checking for updates, so it
 
428
        # should be no problem putting the old UNIX timestamp style of version
 
429
        # number in the version field
 
430
        if self.request.cfg.sitename == 'MoinMaster' and pagename == 'BadContent':
 
431
            version = int(mtime)
 
432
        #######################################################################
 
433
 
 
434
        return {
 
435
            'name': self._outstr(page.page_name),
 
436
            'lastModified': xmlrpclib.DateTime(gmtuple),
 
437
            'author': self._outstr(edit_info['editor']),
 
438
            'version': version,
 
439
            }
 
440
 
 
441
    def xmlrpc_getPage(self, pagename):
 
442
        """ Invoke xmlrpc_getPageVersion with rev=None """
 
443
        return self.xmlrpc_getPageVersion(pagename, rev=None)
 
444
 
 
445
    def xmlrpc_getPageVersion(self, pagename, rev):
 
446
        """ Get raw text from specific revision of pagename
 
447
 
 
448
        @param pagename: pagename (utf-8)
 
449
        @param rev: revision number (int)
 
450
        @rtype: str
 
451
        @return: utf-8 encoded page data
 
452
        """
 
453
        pagename = self._instr(pagename)
 
454
 
 
455
        # User may read page?
 
456
        if not self.request.user.may.read(pagename):
 
457
            return self.notAllowedFault()
 
458
 
 
459
        if rev is not None:
 
460
            page = Page(self.request, pagename, rev=rev)
 
461
        else:
 
462
            page = Page(self.request, pagename)
 
463
 
 
464
        # Non existing page?
 
465
        if not page.exists():
 
466
            return self.noSuchPageFault()
 
467
 
 
468
        # Return page raw text
 
469
        if self.version == 2:
 
470
            return self._outstr(page.get_raw_body())
 
471
        elif self.version == 1:
 
472
            return self._outlob(page.get_raw_body())
 
473
 
 
474
    def xmlrpc_getPageHTML(self, pagename):
 
475
        """ Invoke xmlrpc_getPageHTMLVersion with rev=None """
 
476
        return self.xmlrpc_getPageHTMLVersion(pagename, rev=None)
 
477
 
 
478
    def xmlrpc_getPageHTMLVersion(self, pagename, rev):
 
479
        """ Get HTML of from specific revision of pagename
 
480
 
 
481
        @param pagename: the page name (utf-8)
 
482
        @param rev: revision number (int)
 
483
        @rtype: str
 
484
        @return: page in rendered HTML (utf-8)
 
485
        """
 
486
        pagename = self._instr(pagename)
 
487
 
 
488
        # User may read page?
 
489
        if not self.request.user.may.read(pagename):
 
490
            return self.notAllowedFault()
 
491
 
 
492
        if rev is not None:
 
493
            page = Page(self.request, pagename, rev=rev)
 
494
        else:
 
495
            page = Page(self.request, pagename)
 
496
 
 
497
        # Non existing page?
 
498
        if not page.exists():
 
499
            return self.noSuchPageFault()
 
500
 
 
501
        # Render page into a buffer
 
502
        result = self.request.redirectedOutput(page.send_page, content_only=1)
 
503
 
 
504
        # Return rendered page
 
505
        if self.version == 2:
 
506
            return self._outstr(result)
 
507
        elif self.version == 1:
 
508
            return xmlrpclib.Binary(result)
 
509
 
 
510
    def xmlrpc_listLinks(self, pagename):
 
511
        """
 
512
        list links for a given page
 
513
        @param pagename: the page name
 
514
        @rtype: list
 
515
        @return: links of the page, structs, with the following elements
 
516
            * name (string) : The page name or URL the link is to, UTF-8 encoding.
 
517
            * type (int) : The link type. Zero (0) for internal Wiki
 
518
              link, one (1) for external link (URL - image link, whatever).
 
519
        """
 
520
        pagename = self._instr(pagename)
 
521
 
 
522
        # User may read page?
 
523
        if not self.request.user.may.read(pagename):
 
524
            return self.notAllowedFault()
 
525
 
 
526
        page = Page(self.request, pagename)
 
527
 
 
528
        # Non existing page?
 
529
        if not page.exists():
 
530
            return self.noSuchPageFault()
 
531
 
 
532
        links_out = []
 
533
        for link in page.getPageLinks(self.request):
 
534
            links_out.append({'name': self._outstr(link), 'type': 0 })
 
535
        return links_out
 
536
 
 
537
    def xmlrpc_putPage(self, pagename, pagetext):
 
538
        """
 
539
        save a page / change a page to a new text
 
540
        @param pagename: the page name (unicode or utf-8)
 
541
        @param pagetext: the new page text (content, unicode or utf-8)
 
542
        @rtype: bool
 
543
        @return: true on success
 
544
        """
 
545
 
 
546
        pagename = self._instr(pagename)
 
547
 
 
548
        if not pagename:
 
549
            return xmlrpclib.Fault("INVALID", "pagename can't be empty")
 
550
 
 
551
        # check ACLs
 
552
        if not self.request.user.may.write(pagename):
 
553
            return xmlrpclib.Fault(1, "You are not allowed to edit this page")
 
554
 
 
555
        page = PageEditor(self.request, pagename)
 
556
        try:
 
557
            if self.version == 2:
 
558
                newtext = self._instr(pagetext)
 
559
            elif self.version == 1:
 
560
                newtext = self._inlob(pagetext)
 
561
            msg = page.saveText(newtext, 0)
 
562
        except page.SaveError, msg:
 
563
            logging.error("SaveError: %s" % msg)
 
564
            return xmlrpclib.Fault(1, "%s" % msg)
 
565
 
 
566
        # Update pagelinks cache
 
567
        page.getPageLinks(self.request)
 
568
 
 
569
        return xmlrpclib.Boolean(1)
 
570
 
 
571
    def xmlrpc_revertPage(self, pagename, revision):
 
572
        """Revert a page to previous revision
 
573
 
 
574
        This is mainly intended to be used by the jabber bot.
 
575
 
 
576
        @param pagename: the page name (unicode or utf-8)
 
577
        @param revision: revision to revert to
 
578
        @rtype bool
 
579
        @return true on success
 
580
 
 
581
        """
 
582
        if not self.request.user.may.write(pagename):
 
583
            return xmlrpclib.Fault(1, "You are not allowed to edit this page")
 
584
 
 
585
        rev = int(self._instr(revision))
 
586
        editor = PageEditor(self.request, pagename)
 
587
 
 
588
        try:
 
589
            editor.revertPage(rev)
 
590
        except PageEditor.SaveError, error:
 
591
            return xmlrpclib.Fault(1, "Revert failed: %s" % (str(error), ))
 
592
 
 
593
        return xmlrpclib.Boolean(1)
 
594
 
 
595
    def xmlrpc_searchPages(self, query_string):
 
596
        """ Searches pages for query_string.
 
597
            Returns a list of tuples (foundpagename, context)
 
598
        """
 
599
        from MoinMoin import search
 
600
        results = search.searchPages(self.request, query_string)
 
601
        results.formatter = self.request.html_formatter
 
602
        results.request = self.request
 
603
        return [(self._outstr(hit.page_name),
 
604
                 self._outstr(results.formatContext(hit, 180, 1)))
 
605
                for hit in results.hits]
 
606
 
 
607
    def xmlrpc_searchPagesEx(self, query_string, search_type, length, case, mtime, regexp):
 
608
        """ Searches pages for query_string - extended version for compatibility
 
609
 
 
610
        This function, in contrary to searchPages(), doesn't return HTML-formatted data.
 
611
 
 
612
        @param query_string: term to search for
 
613
        @param search_type: "text" or "title" search
 
614
        @param length: length of context preview (in characters)
 
615
        @param case: should the search be case sensitive?
 
616
        @param mtime: only output pages modified after mtime
 
617
        @param regexp: should the query_string be treates as a regular expression?
 
618
        @return: (page name, context preview, page url)
 
619
 
 
620
        """
 
621
        from MoinMoin import search
 
622
        from MoinMoin.formatter.text_plain import Formatter
 
623
 
 
624
        kwargs = {"sort": "page_name", "case": case, "regex": regexp}
 
625
        if search_type == "title":
 
626
            kwargs["titlesearch"] = True
 
627
 
 
628
        results = search.searchPages(self.request, query_string, **kwargs)
 
629
        results.formatter = Formatter(self.request)
 
630
        results.request = self.request
 
631
 
 
632
        return [(self._outstr(hit.page_name),
 
633
                 self._outstr(results.formatContext(hit, length, 1)),
 
634
                 self.request.getQualifiedURL(hit.page.url(self.request, {})))
 
635
                for hit in results.hits]
 
636
 
 
637
    def xmlrpc_getMoinVersion(self):
 
638
        """ Returns a tuple of the MoinMoin version:
 
639
            (project, release, revision)
 
640
        """
 
641
        from MoinMoin import version
 
642
        return (version.project, version.release, version.revision)
 
643
 
 
644
 
 
645
    # user profile data transfer
 
646
 
 
647
    def xmlrpc_getUserProfile(self):
 
648
        """ Return the user profile data for the current user.
 
649
            Use this in a single multicall after applyAuthToken.
 
650
            If we have a valid user, returns a dict of items from user profile.
 
651
            Otherwise, return an empty dict.
 
652
        """
 
653
        u = self.request.user
 
654
        if not u.valid:
 
655
            userdata = {}
 
656
        else:
 
657
            userdata = dict(u.persistent_items())
 
658
        return userdata
 
659
 
 
660
    def xmlrpc_getUserLanguageByJID(self, jid):
 
661
        """ Returns user's language given his/her Jabber ID
 
662
 
 
663
        It makes no sense to consider this a secret, right? Therefore
 
664
        an authentication token is not required. We return a default
 
665
        of "en" if user is not found.
 
666
 
 
667
        TODO: surge protection? Do we fear account enumeration?
 
668
        """
 
669
        retval = "en"
 
670
        u = user.get_by_jabber_id(self.request, jid)
 
671
        if u:
 
672
            retval = u.language
 
673
 
 
674
        return retval
 
675
 
 
676
    # authorization methods
 
677
 
 
678
    def xmlrpc_getAuthToken(self, username, password, *args):
 
679
        """ Returns a token which can be used for authentication
 
680
            in other XMLRPC calls. If the token is empty, the username
 
681
            or the password were wrong.
 
682
        """
 
683
        id_handler = XmlRpcAuthTokenIDHandler()
 
684
 
 
685
        u = self.request.cfg.session_handler.start(self.request, id_handler)
 
686
        u = self.request.handle_auth(u, username=username,
 
687
                                     password=password, login=True)
 
688
 
 
689
        self.request.cfg.session_handler.after_auth(self.request, id_handler, u)
 
690
 
 
691
        if u and u.valid:
 
692
            return id_handler.token
 
693
        else:
 
694
            return ""
 
695
 
 
696
    def xmlrpc_getJabberAuthToken(self, jid, secret):
 
697
        """Returns a token which can be used for authentication.
 
698
 
 
699
        This token can be used in other XMLRPC calls. Generation of
 
700
        token depends on user's JID and a secret shared between wiki
 
701
        and Jabber bot.
 
702
 
 
703
        @param jid: a bare Jabber ID
 
704
        """
 
705
        if self.cfg.secret != secret:
 
706
            return ""
 
707
 
 
708
        u = self.request.handle_jid_auth(jid)
 
709
 
 
710
        if u and u.valid:
 
711
            return self._generate_auth_token(u)
 
712
        else:
 
713
            return ""
 
714
 
 
715
    def xmlrpc_applyAuthToken(self, auth_token):
 
716
        """ Applies the auth token and thereby authenticates the user. """
 
717
        if not auth_token:
 
718
            return xmlrpclib.Fault("INVALID", "Empty token.")
 
719
 
 
720
        id_handler = XmlRpcAuthTokenIDHandler(auth_token)
 
721
 
 
722
        u = self.request.cfg.session_handler.start(self.request, id_handler)
 
723
        u = self.request.handle_auth(u)
 
724
        self.request.cfg.session_handler.after_auth(self.request, id_handler, u)
 
725
        if u and u.valid:
 
726
            self.request.user = u
 
727
            return "SUCCESS"
 
728
        else:
 
729
            return xmlrpclib.Fault("INVALID", "Invalid token.")
 
730
 
 
731
 
 
732
    def xmlrpc_deleteAuthToken(self, auth_token):
 
733
        """ Delete the given auth token. """
 
734
        id_handler = XmlRpcAuthTokenIDHandler(auth_token)
 
735
 
 
736
        u = self.request.cfg.session_handler.start(self.request, id_handler)
 
737
        u = self.request.handle_auth(u)
 
738
        self.request.cfg.session_handler.after_auth(self.request, id_handler, u)
 
739
 
 
740
        self.request.session.delete()
 
741
 
 
742
        return "SUCCESS"
 
743
 
 
744
 
 
745
    # methods for wiki synchronization
 
746
 
 
747
    def xmlrpc_getDiff(self, pagename, from_rev, to_rev, n_name=None):
 
748
        """ Gets the binary difference between two page revisions.
 
749
 
 
750
            @param pagename: unicode string qualifying the page name
 
751
 
 
752
            @param fromRev: integer specifying the source revision. May be None to
 
753
            refer to a virtual empty revision which leads to a diff
 
754
            containing the whole page.
 
755
 
 
756
            @param toRev: integer specifying the target revision. May be None to
 
757
            refer to the current revision. If the current revision is the same
 
758
            as fromRev, there will be a special error condition "ALREADY_CURRENT"
 
759
 
 
760
            @param n_name: do a tag check verifying that n_name was the normalised
 
761
            name of the last tag
 
762
 
 
763
            If both fromRev and toRev are None, this function acts similar to getPage, i.e. it will diff("",currentRev).
 
764
 
 
765
            @return Returns a dict:
 
766
            * status (not a field, implicit, returned as Fault if not SUCCESS):
 
767
             * "SUCCESS" - if the diff could be retrieved successfully
 
768
             * "NOT_EXIST" - item does not exist
 
769
             * "FROMREV_INVALID" - the source revision is invalid
 
770
             * "TOREV_INVALID" - the target revision is invalid
 
771
             * "INTERNAL_ERROR" - there was an internal error
 
772
             * "INVALID_TAG" - the last tag does not match the supplied normalised name
 
773
             * "ALREADY_CURRENT" - this not merely an error condition. It rather means that
 
774
             there is no new revision to diff against which is a good thing while
 
775
             synchronisation.
 
776
            * current: the revision number of the current revision (not the one which was diff'ed against)
 
777
            * diff: Binary object that transports a zlib-compressed binary diff (see bdiff.py, taken from Mercurial)
 
778
            * conflict: if there is a conflict on the page currently
 
779
 
 
780
        """
 
781
        from MoinMoin.util.bdiff import textdiff, compress
 
782
        from MoinMoin.wikisync import TagStore
 
783
 
 
784
        pagename = self._instr(pagename)
 
785
        if n_name is not None:
 
786
            n_name = self._instr(n_name)
 
787
 
 
788
        # User may read page?
 
789
        if not self.request.user.may.read(pagename):
 
790
            return self.notAllowedFault()
 
791
 
 
792
        def allowed_rev_type(data):
 
793
            if data is None:
 
794
                return True
 
795
            return isinstance(data, int) and data > 0
 
796
 
 
797
        if not allowed_rev_type(from_rev):
 
798
            return xmlrpclib.Fault("FROMREV_INVALID", "Incorrect type for from_rev.")
 
799
 
 
800
        if not allowed_rev_type(to_rev):
 
801
            return xmlrpclib.Fault("TOREV_INVALID", "Incorrect type for to_rev.")
 
802
 
 
803
        currentpage = Page(self.request, pagename)
 
804
        if not currentpage.exists():
 
805
            return xmlrpclib.Fault("NOT_EXIST", "Page does not exist.")
 
806
 
 
807
        revisions = currentpage.getRevList()
 
808
 
 
809
        if from_rev is not None and from_rev not in revisions:
 
810
            return xmlrpclib.Fault("FROMREV_INVALID", "Unknown from_rev.")
 
811
        if to_rev is not None and to_rev not in revisions:
 
812
            return xmlrpclib.Fault("TOREV_INVALID", "Unknown to_rev.")
 
813
 
 
814
        # use lambda to defer execution in the next lines
 
815
        if from_rev is None:
 
816
            oldcontents = lambda: ""
 
817
        else:
 
818
            oldpage = Page(self.request, pagename, rev=from_rev)
 
819
            oldcontents = lambda: oldpage.get_raw_body_str()
 
820
 
 
821
        if to_rev is None:
 
822
            newpage = currentpage
 
823
            newcontents = lambda: currentpage.get_raw_body_str()
 
824
        else:
 
825
            newpage = Page(self.request, pagename, rev=to_rev)
 
826
            newcontents = lambda: newpage.get_raw_body_str()
 
827
 
 
828
        if oldcontents() and oldpage.get_real_rev() == newpage.get_real_rev():
 
829
            return xmlrpclib.Fault("ALREADY_CURRENT", "There are no changes.")
 
830
 
 
831
        if n_name is not None:
 
832
            tags = TagStore(newpage)
 
833
            last_tag = tags.get_last_tag()
 
834
            if last_tag is not None and last_tag.normalised_name != n_name:
 
835
                return xmlrpclib.Fault("INVALID_TAG", "The used tag is incorrect because the normalised name does not match.")
 
836
 
 
837
        newcontents = newcontents()
 
838
        conflict = wikiutil.containsConflictMarker(newcontents)
 
839
        diffblob = xmlrpclib.Binary(compress(textdiff(oldcontents(), newcontents)))
 
840
 
 
841
        return {"conflict": conflict, "diff": diffblob, "diffversion": 1, "current": currentpage.get_real_rev()}
 
842
 
 
843
    def xmlrpc_interwikiName(self):
 
844
        """ Returns the interwiki name and the IWID of the current wiki. """
 
845
        name = self.request.cfg.interwikiname
 
846
        iwid = self.request.cfg.iwid
 
847
        if name is None:
 
848
            return [None, iwid]
 
849
        else:
 
850
            return [self._outstr(name), iwid]
 
851
 
 
852
    def xmlrpc_mergeDiff(self, pagename, diff, local_rev, delta_remote_rev, last_remote_rev, interwiki_name, normalised_name):
 
853
        """ Merges a diff sent by the remote machine and returns the number of the new revision.
 
854
            Additionally, this method tags the new revision.
 
855
 
 
856
            @param pagename: The pagename that is currently dealt with.
 
857
            @param diff: The diff that can be applied to the version specified by delta_remote_rev.
 
858
                If it is None, the page is deleted.
 
859
            @param local_rev: The revno of the page on the other wiki system, used for the tag.
 
860
            @param delta_remote_rev: The revno that the diff is taken against.
 
861
            @param last_remote_rev: The last revno of the page `pagename` that is known by the other wiki site.
 
862
            @param interwiki_name: Used to build the interwiki tag.
 
863
            @param normalised_name: The normalised pagename that is common to both wikis.
 
864
 
 
865
            @return Returns the current revision number after the merge was done. Or one of the following errors:
 
866
                * "SUCCESS" - the page could be merged and tagged successfully.
 
867
                * "NOT_EXIST" - item does not exist and there was not any content supplied.
 
868
                * "LASTREV_INVALID" - the page was changed and the revision got invalid
 
869
                * "INTERNAL_ERROR" - there was an internal error
 
870
                * "NOT_ALLOWED" - you are not allowed to do the merge operation on the page
 
871
        """
 
872
        from MoinMoin.util.bdiff import decompress, patch
 
873
        from MoinMoin.wikisync import TagStore, BOTH
 
874
        from MoinMoin.packages import unpackLine
 
875
        LASTREV_INVALID = xmlrpclib.Fault("LASTREV_INVALID", "The page was changed")
 
876
 
 
877
        pagename = self._instr(pagename)
 
878
 
 
879
        comment = u"Remote Merge - %r" % unpackLine(interwiki_name)[-1]
 
880
 
 
881
        # User may read page?
 
882
        if not self.request.user.may.read(pagename) or not self.request.user.may.write(pagename):
 
883
            return xmlrpclib.Fault("NOT_ALLOWED", "You are not allowed to write to this page.")
 
884
 
 
885
        # XXX add locking here!
 
886
 
 
887
        # current version of the page
 
888
        currentpage = PageEditor(self.request, pagename, do_editor_backup=0)
 
889
 
 
890
        if last_remote_rev is not None and currentpage.get_real_rev() != last_remote_rev:
 
891
            return LASTREV_INVALID
 
892
 
 
893
        if not currentpage.exists() and diff is None:
 
894
            return xmlrpclib.Fault("NOT_EXIST", "The page does not exist and no diff was supplied.")
 
895
 
 
896
        if diff is None: # delete the page
 
897
            try:
 
898
                currentpage.deletePage(comment)
 
899
            except PageEditor.AccessDenied, (msg, ):
 
900
                return xmlrpclib.Fault("NOT_ALLOWED", msg)
 
901
            return currentpage.get_real_rev()
 
902
 
 
903
        # base revision used for the diff
 
904
        basepage = Page(self.request, pagename, rev=(delta_remote_rev or 0))
 
905
 
 
906
        # generate the new page revision by applying the diff
 
907
        newcontents = patch(basepage.get_raw_body_str(), decompress(str(diff)))
 
908
        #print "Diff against %r" % basepage.get_raw_body_str()
 
909
 
 
910
        # write page
 
911
        try:
 
912
            currentpage.saveText(newcontents.decode("utf-8"), last_remote_rev or 0, comment=comment)
 
913
        except PageEditor.Unchanged: # could happen in case of both wiki's pages being equal
 
914
            pass
 
915
        except PageEditor.EditConflict:
 
916
            return LASTREV_INVALID
 
917
 
 
918
        current_rev = currentpage.get_real_rev()
 
919
 
 
920
        tags = TagStore(currentpage)
 
921
        tags.add(remote_wiki=interwiki_name, remote_rev=local_rev, current_rev=current_rev, direction=BOTH, normalised_name=normalised_name)
 
922
 
 
923
        # XXX unlock page
 
924
 
 
925
        return current_rev
 
926
 
 
927
 
 
928
    # XXX BEGIN WARNING XXX
 
929
    # All xmlrpc_*Attachment* functions have to be considered as UNSTABLE API -
 
930
    # they are neither standard nor are they what we need when we have switched
 
931
    # attachments (1.5 style) to mimetype items (hopefully in 1.6).
 
932
    # They will be partly removed, esp. the semantics of the function "listAttachments"
 
933
    # cannot be sensibly defined for items.
 
934
    # If the first beta or more stable release of 1.6 will have new item semantics,
 
935
    # we will remove the functions before it is released.
 
936
    def xmlrpc_listAttachments(self, pagename):
 
937
        """ Get all attachments associated with pagename
 
938
        Deprecated.
 
939
 
 
940
        @param pagename: pagename (utf-8)
 
941
        @rtype: list
 
942
        @return: a list of utf-8 attachment names
 
943
        """
 
944
        pagename = self._instr(pagename)
 
945
        # User may read page?
 
946
        if not self.request.user.may.read(pagename):
 
947
            return self.notAllowedFault()
 
948
 
 
949
        result = AttachFile._get_files(self.request, pagename)
 
950
        return result
 
951
 
 
952
    def xmlrpc_getAttachment(self, pagename, attachname):
 
953
        """ Get attachname associated with pagename
 
954
 
 
955
        @param pagename: pagename (utf-8)
 
956
        @param attachname: attachment name (utf-8)
 
957
        @rtype base64
 
958
        @return base64 data
 
959
        """
 
960
        pagename = self._instr(pagename)
 
961
        # User may read page?
 
962
        if not self.request.user.may.read(pagename):
 
963
            return self.notAllowedFault()
 
964
 
 
965
        filename = wikiutil.taintfilename(self._instr(attachname))
 
966
        filename = AttachFile.getFilename(self.request, pagename, filename)
 
967
        if not os.path.isfile(filename):
 
968
            return self.noSuchPageFault()
 
969
        return self._outlob(open(filename, 'rb').read())
 
970
 
 
971
    def xmlrpc_putAttachment(self, pagename, attachname, data):
 
972
        """ Set attachname associated with pagename to data
 
973
 
 
974
        @param pagename: pagename (utf-8)
 
975
        @param attachname: attachment name (utf-8)
 
976
        @param data: file data (base64)
 
977
        @rtype boolean
 
978
        @return True if attachment was set
 
979
        """
 
980
        pagename = self._instr(pagename)
 
981
        # User may read page?
 
982
        if not self.request.user.may.read(pagename):
 
983
            return self.notAllowedFault()
 
984
 
 
985
        # also check ACLs
 
986
        if not self.request.user.may.write(pagename):
 
987
            return xmlrpclib.Fault(1, "You are not allowed to edit this page")
 
988
 
 
989
        attachname = wikiutil.taintfilename(attachname)
 
990
        filename = AttachFile.getFilename(self.request, pagename, attachname)
 
991
        if os.path.exists(filename) and not os.path.isfile(filename):
 
992
            return self.noSuchPageFault()
 
993
        open(filename, 'wb+').write(data.data)
 
994
        AttachFile._addLogEntry(self.request, 'ATTNEW', pagename, filename)
 
995
        return xmlrpclib.Boolean(1)
 
996
 
 
997
    # XXX END WARNING XXX
 
998
 
 
999
 
 
1000
    def xmlrpc_getBotTranslations(self):
 
1001
        """ Return translations to be used by notification bot
 
1002
 
 
1003
        @return: a dict (indexed by language) of dicts of translated strings (indexed by original ones)
 
1004
        """
 
1005
        from MoinMoin.i18n import bot_translations
 
1006
        return bot_translations(self.request)
 
1007
 
 
1008
 
 
1009
class XmlRpc1(XmlRpcBase):
 
1010
 
 
1011
    def __init__(self, request):
 
1012
        XmlRpcBase.__init__(self, request)
 
1013
        self.version = 1
 
1014
 
 
1015
    def _instr(self, text):
 
1016
        """ Convert string we get from xmlrpc into internal representation
 
1017
 
 
1018
        @param text: quoted text (str or unicode object)
 
1019
        @rtype: unicode
 
1020
        @return: text
 
1021
        """
 
1022
        return wikiutil.url_unquote(text) # config.charset must be utf-8
 
1023
 
 
1024
    def _outstr(self, text):
 
1025
        """ Convert string from internal representation to xmlrpc
 
1026
 
 
1027
        @param text: unicode or string in config.charset
 
1028
        @rtype: str
 
1029
        @return: text encoded in utf-8 and quoted
 
1030
        """
 
1031
        return wikiutil.url_quote(text) # config.charset must be utf-8
 
1032
 
 
1033
 
 
1034
class XmlRpc2(XmlRpcBase):
 
1035
 
 
1036
    def __init__(self, request):
 
1037
        XmlRpcBase.__init__(self, request)
 
1038
        self.version = 2
 
1039
 
 
1040
    def _instr(self, text):
 
1041
        """ Convert string we get from xmlrpc into internal representation
 
1042
 
 
1043
        @param text: unicode or utf-8 string
 
1044
        @rtype: unicode
 
1045
        @return: text
 
1046
        """
 
1047
        if not isinstance(text, unicode):
 
1048
            text = unicode(text, 'utf-8')
 
1049
        return text
 
1050
 
 
1051
    def _outstr(self, text):
 
1052
        """ Convert string from internal representation to xmlrpc
 
1053
 
 
1054
        @param text: unicode or string in config.charset
 
1055
        @rtype: str
 
1056
        @return: text encoded in utf-8
 
1057
        """
 
1058
        if isinstance(text, unicode):
 
1059
            text = text.encode('utf-8')
 
1060
        elif config.charset != 'utf-8':
 
1061
            text = unicode(text, config.charset).encode('utf-8')
 
1062
        return text
 
1063
 
 
1064
 
 
1065
def xmlrpc(request):
 
1066
    XmlRpc1(request).process()
 
1067
 
 
1068
 
 
1069
def xmlrpc2(request):
 
1070
    XmlRpc2(request).process()
 
1071