1
1
# -*- coding: iso-8859-1 -*-
3
MoinMoin - xmlrpc extension modules Package
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
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.
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.
12
See also http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2
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
20
@copyright: 2003-2008 MoinMoin:ThomasWaldmann,
21
2004-2006 MoinMoin:AlexanderSchremmer
22
@license: GNU GPL, see COPYING for details
8
24
from MoinMoin.util import pysupport
10
26
modules = pysupport.getPackageModules(__file__)
28
import os, sys, time, xmlrpclib
30
from MoinMoin import log
31
logging = log.getLogger(__name__)
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
42
class XmlRpcAuthTokenIDHandler(session.SessionIDHandler):
43
def __init__(self, token=None):
44
session.SessionIDHandler.__init__(self)
47
def get(self, request):
50
def set(self, request, session_id, expires):
51
self.token = session_id
54
logging_tearline = '- XMLRPC %s ' + '-' * 40
57
""" XMLRPC base class with common functionality of wiki xmlrpc v1 and v2 """
58
def __init__(self, request):
60
Initialize an XmlRpcBase object.
61
@param request: the request object
63
self.request = request
64
self.version = None # this has to be defined in derived class
65
self.cfg = request.cfg
67
#############################################################################
69
#############################################################################
71
def _instr(self, text):
72
""" Convert inbound string.
74
@param text: the text to convert (encoded str or unicode)
76
@return: text as unicode
78
raise NotImplementedError("please implement _instr in derived class")
80
def _outstr(self, text):
81
""" Convert outbound string.
83
@param text: the text to convert (encoded str or unicode)
85
@return: text as encoded str
87
raise NotImplementedError("please implement _outstr in derived class")
89
def _inlob(self, text):
90
""" Convert inbound base64-encoded utf-8 to Large OBject.
92
@param text: the text to convert
96
text = text.data #this is a already base64-decoded 8bit string
97
text = unicode(text, 'utf-8')
100
def _outlob(self, text):
101
""" Convert outbound Large OBject to base64-encoded utf-8.
103
@param text: the text, either unicode or utf-8 string
105
@return: xmlrpc Binary object
107
if isinstance(text, unicode):
108
text = text.encode('utf-8')
110
if config.charset != 'utf-8':
111
text = unicode(text, config.charset).encode('utf-8')
112
return xmlrpclib.Binary(text)
115
""" Convert an exception to a string.
118
@return: traceback as string
122
return "%s: %s\n%s" % (
125
'\n'.join(traceback.format_tb(sys.exc_info()[2])),
129
""" xmlrpc v1 and v2 dispatcher """
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.")
135
data = self.request.read(self.request.content_length)
138
params, method = xmlrpclib.loads(data)
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')
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')
151
response = self.dispatch(method, params)
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()))
157
logging.debug(logging_tearline % 'response begin')
158
logging.debug(response)
159
logging.debug(logging_tearline % 'response end')
161
if isinstance(response, xmlrpclib.Fault):
162
response = xmlrpclib.dumps(response)
164
# wrap response in a singleton tuple
165
response = (response, )
167
response = xmlrpclib.dumps(response, methodresponse=1, allow_none=True)
169
self.request.emit_http_headers([
170
"Content-Type: text/xml; charset=utf-8",
171
"Content-Length: %d" % len(response),
173
self.request.write(response)
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
179
method = method.replace(".", "_")
182
fn = getattr(self, 'xmlrpc_' + method)
183
except AttributeError:
185
fn = wikiutil.importPlugin(self.request.cfg, 'xmlrpc',
187
except wikiutil.PluginMissingError:
188
response = xmlrpclib.Fault(1, "No such method: %s." %
191
response = fn(self, *params)
193
response = fn(*params)
197
# Common faults -----------------------------------------------------
199
def notAllowedFault(self):
200
return xmlrpclib.Fault(1, "You are not allowed to read this page.")
202
def noSuchPageFault(self):
203
return xmlrpclib.Fault(1, "No such page was found.")
205
def noLogEntryFault(self):
206
return xmlrpclib.Fault(1, "No log entry was found.")
208
#############################################################################
210
#############################################################################
212
def xmlrpc_system_multicall(self, call_list):
213
"""system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => [[4], ...]
215
Allows the caller to package multiple XML-RPC calls into a single
218
See http://www.xmlrpc.com/discuss/msgReader$1208
220
Copied from SimpleXMLRPCServer.py
224
for call in call_list:
225
method_name = call['methodName']
226
params = call['params']
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)
233
if not isinstance(result, xmlrpclib.Fault):
234
results.append([result])
237
{'faultCode': result.faultCode,
238
'faultString': result.faultString}
243
'faultString': "%s:%s" % (sys.exc_type, sys.exc_value)}
248
#############################################################################
249
### Interface implementation
250
#############################################################################
252
def xmlrpc_getRPCVersionSupported(self):
253
""" Returns version of the Wiki API.
256
@return: 1 or 2 (wikirpc version)
260
def xmlrpc_getAllPages(self):
261
""" Get all pages readable by current user
264
@return: a list of all pages.
267
# the official WikiRPC interface is implemented by the extended method
269
return self.xmlrpc_getAllPagesEx()
272
def xmlrpc_getAllPagesEx(self, opts=None):
273
""" Get all pages readable by current user. Not an WikiRPC method.
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.
285
@return: a list of all pages.
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}
294
if not options["include_system"]:
295
p_filter = lambda name: not wikiutil.isSystemPage(self.request, name)
297
p_filter = lambda name: True
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)
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):
306
n_name = normalise_pagename(name, prefix)
311
return n_name in pagelist
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"])
317
if options['include_revno']:
319
for page in pagelist:
320
revno = page.get_real_rev()
321
if options["mark_deleted"] and not page.exists():
323
pages.append([self._outstr(page.page_name), revno])
326
return [self._outstr(page) for page in pagelist]
328
def xmlrpc_getRecentChanges(self, date):
329
""" Get RecentChanges since date
331
@param date: date since when rc will be listed
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:
336
Name of the page. The name is in UTF-8.
337
* lastModified (date) :
338
Date of last modification, in UTC.
340
Name of the author (if available). UTF-8.
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)
353
# skip if older than "date"
354
if lastModified_date < date:
357
# skip if knowledge not permitted
358
if not self.request.user.may.read(log.pagename):
361
# get page name (str) from log
362
pagename_str = self._outstr(log.pagename)
364
# get user name (str) from log
365
author_str = log.hostname
367
userdata = user.User(self.request, log.userid)
369
author_str = userdata.name
370
author_str = self._outstr(author_str)
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)
380
def xmlrpc_getPageInfo(self, pagename):
381
""" Invoke xmlrpc_getPageInfoVersion with rev=None """
382
return self.xmlrpc_getPageInfoVersion(pagename, rev=None)
384
def xmlrpc_getPageInfoVersion(self, pagename, rev):
385
""" Return page information for specific revision
387
@param pagename: the name of the page (utf-8)
388
@param rev: revision to get info about (int)
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
397
pn = self._instr(pagename)
399
# User may read this page?
400
if not self.request.user.may.read(pn):
401
return self.notAllowedFault()
404
page = Page(self.request, pn, rev=rev)
406
page = Page(self.request, pn)
407
rev = page.current_rev()
410
if not page.exists():
411
return self.noSuchPageFault()
414
edit_info = page.edit_info()
416
return self.noLogEntryFault()
418
mtime = wikiutil.version2timestamp(long(edit_info['timestamp'])) # must be long for py 2.2.x
419
gmtuple = tuple(time.gmtime(mtime))
421
version = rev # our new rev numbers: 1,2,3,4,....
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':
432
#######################################################################
435
'name': self._outstr(page.page_name),
436
'lastModified': xmlrpclib.DateTime(gmtuple),
437
'author': self._outstr(edit_info['editor']),
441
def xmlrpc_getPage(self, pagename):
442
""" Invoke xmlrpc_getPageVersion with rev=None """
443
return self.xmlrpc_getPageVersion(pagename, rev=None)
445
def xmlrpc_getPageVersion(self, pagename, rev):
446
""" Get raw text from specific revision of pagename
448
@param pagename: pagename (utf-8)
449
@param rev: revision number (int)
451
@return: utf-8 encoded page data
453
pagename = self._instr(pagename)
455
# User may read page?
456
if not self.request.user.may.read(pagename):
457
return self.notAllowedFault()
460
page = Page(self.request, pagename, rev=rev)
462
page = Page(self.request, pagename)
465
if not page.exists():
466
return self.noSuchPageFault()
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())
474
def xmlrpc_getPageHTML(self, pagename):
475
""" Invoke xmlrpc_getPageHTMLVersion with rev=None """
476
return self.xmlrpc_getPageHTMLVersion(pagename, rev=None)
478
def xmlrpc_getPageHTMLVersion(self, pagename, rev):
479
""" Get HTML of from specific revision of pagename
481
@param pagename: the page name (utf-8)
482
@param rev: revision number (int)
484
@return: page in rendered HTML (utf-8)
486
pagename = self._instr(pagename)
488
# User may read page?
489
if not self.request.user.may.read(pagename):
490
return self.notAllowedFault()
493
page = Page(self.request, pagename, rev=rev)
495
page = Page(self.request, pagename)
498
if not page.exists():
499
return self.noSuchPageFault()
501
# Render page into a buffer
502
result = self.request.redirectedOutput(page.send_page, content_only=1)
504
# Return rendered page
505
if self.version == 2:
506
return self._outstr(result)
507
elif self.version == 1:
508
return xmlrpclib.Binary(result)
510
def xmlrpc_listLinks(self, pagename):
512
list links for a given page
513
@param pagename: the page name
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).
520
pagename = self._instr(pagename)
522
# User may read page?
523
if not self.request.user.may.read(pagename):
524
return self.notAllowedFault()
526
page = Page(self.request, pagename)
529
if not page.exists():
530
return self.noSuchPageFault()
533
for link in page.getPageLinks(self.request):
534
links_out.append({'name': self._outstr(link), 'type': 0 })
537
def xmlrpc_putPage(self, pagename, pagetext):
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)
543
@return: true on success
546
pagename = self._instr(pagename)
549
return xmlrpclib.Fault("INVALID", "pagename can't be empty")
552
if not self.request.user.may.write(pagename):
553
return xmlrpclib.Fault(1, "You are not allowed to edit this page")
555
page = PageEditor(self.request, pagename)
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)
566
# Update pagelinks cache
567
page.getPageLinks(self.request)
569
return xmlrpclib.Boolean(1)
571
def xmlrpc_revertPage(self, pagename, revision):
572
"""Revert a page to previous revision
574
This is mainly intended to be used by the jabber bot.
576
@param pagename: the page name (unicode or utf-8)
577
@param revision: revision to revert to
579
@return true on success
582
if not self.request.user.may.write(pagename):
583
return xmlrpclib.Fault(1, "You are not allowed to edit this page")
585
rev = int(self._instr(revision))
586
editor = PageEditor(self.request, pagename)
589
editor.revertPage(rev)
590
except PageEditor.SaveError, error:
591
return xmlrpclib.Fault(1, "Revert failed: %s" % (str(error), ))
593
return xmlrpclib.Boolean(1)
595
def xmlrpc_searchPages(self, query_string):
596
""" Searches pages for query_string.
597
Returns a list of tuples (foundpagename, context)
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]
607
def xmlrpc_searchPagesEx(self, query_string, search_type, length, case, mtime, regexp):
608
""" Searches pages for query_string - extended version for compatibility
610
This function, in contrary to searchPages(), doesn't return HTML-formatted data.
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)
621
from MoinMoin import search
622
from MoinMoin.formatter.text_plain import Formatter
624
kwargs = {"sort": "page_name", "case": case, "regex": regexp}
625
if search_type == "title":
626
kwargs["titlesearch"] = True
628
results = search.searchPages(self.request, query_string, **kwargs)
629
results.formatter = Formatter(self.request)
630
results.request = self.request
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]
637
def xmlrpc_getMoinVersion(self):
638
""" Returns a tuple of the MoinMoin version:
639
(project, release, revision)
641
from MoinMoin import version
642
return (version.project, version.release, version.revision)
645
# user profile data transfer
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.
653
u = self.request.user
657
userdata = dict(u.persistent_items())
660
def xmlrpc_getUserLanguageByJID(self, jid):
661
""" Returns user's language given his/her Jabber ID
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.
667
TODO: surge protection? Do we fear account enumeration?
670
u = user.get_by_jabber_id(self.request, jid)
676
# authorization methods
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.
683
id_handler = XmlRpcAuthTokenIDHandler()
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)
689
self.request.cfg.session_handler.after_auth(self.request, id_handler, u)
692
return id_handler.token
696
def xmlrpc_getJabberAuthToken(self, jid, secret):
697
"""Returns a token which can be used for authentication.
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
703
@param jid: a bare Jabber ID
705
if self.cfg.secret != secret:
708
u = self.request.handle_jid_auth(jid)
711
return self._generate_auth_token(u)
715
def xmlrpc_applyAuthToken(self, auth_token):
716
""" Applies the auth token and thereby authenticates the user. """
718
return xmlrpclib.Fault("INVALID", "Empty token.")
720
id_handler = XmlRpcAuthTokenIDHandler(auth_token)
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)
726
self.request.user = u
729
return xmlrpclib.Fault("INVALID", "Invalid token.")
732
def xmlrpc_deleteAuthToken(self, auth_token):
733
""" Delete the given auth token. """
734
id_handler = XmlRpcAuthTokenIDHandler(auth_token)
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)
740
self.request.session.delete()
745
# methods for wiki synchronization
747
def xmlrpc_getDiff(self, pagename, from_rev, to_rev, n_name=None):
748
""" Gets the binary difference between two page revisions.
750
@param pagename: unicode string qualifying the page name
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.
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"
760
@param n_name: do a tag check verifying that n_name was the normalised
763
If both fromRev and toRev are None, this function acts similar to getPage, i.e. it will diff("",currentRev).
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
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
781
from MoinMoin.util.bdiff import textdiff, compress
782
from MoinMoin.wikisync import TagStore
784
pagename = self._instr(pagename)
785
if n_name is not None:
786
n_name = self._instr(n_name)
788
# User may read page?
789
if not self.request.user.may.read(pagename):
790
return self.notAllowedFault()
792
def allowed_rev_type(data):
795
return isinstance(data, int) and data > 0
797
if not allowed_rev_type(from_rev):
798
return xmlrpclib.Fault("FROMREV_INVALID", "Incorrect type for from_rev.")
800
if not allowed_rev_type(to_rev):
801
return xmlrpclib.Fault("TOREV_INVALID", "Incorrect type for to_rev.")
803
currentpage = Page(self.request, pagename)
804
if not currentpage.exists():
805
return xmlrpclib.Fault("NOT_EXIST", "Page does not exist.")
807
revisions = currentpage.getRevList()
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.")
814
# use lambda to defer execution in the next lines
816
oldcontents = lambda: ""
818
oldpage = Page(self.request, pagename, rev=from_rev)
819
oldcontents = lambda: oldpage.get_raw_body_str()
822
newpage = currentpage
823
newcontents = lambda: currentpage.get_raw_body_str()
825
newpage = Page(self.request, pagename, rev=to_rev)
826
newcontents = lambda: newpage.get_raw_body_str()
828
if oldcontents() and oldpage.get_real_rev() == newpage.get_real_rev():
829
return xmlrpclib.Fault("ALREADY_CURRENT", "There are no changes.")
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.")
837
newcontents = newcontents()
838
conflict = wikiutil.containsConflictMarker(newcontents)
839
diffblob = xmlrpclib.Binary(compress(textdiff(oldcontents(), newcontents)))
841
return {"conflict": conflict, "diff": diffblob, "diffversion": 1, "current": currentpage.get_real_rev()}
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
850
return [self._outstr(name), iwid]
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.
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.
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
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")
877
pagename = self._instr(pagename)
879
comment = u"Remote Merge - %r" % unpackLine(interwiki_name)[-1]
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.")
885
# XXX add locking here!
887
# current version of the page
888
currentpage = PageEditor(self.request, pagename, do_editor_backup=0)
890
if last_remote_rev is not None and currentpage.get_real_rev() != last_remote_rev:
891
return LASTREV_INVALID
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.")
896
if diff is None: # delete the page
898
currentpage.deletePage(comment)
899
except PageEditor.AccessDenied, (msg, ):
900
return xmlrpclib.Fault("NOT_ALLOWED", msg)
901
return currentpage.get_real_rev()
903
# base revision used for the diff
904
basepage = Page(self.request, pagename, rev=(delta_remote_rev or 0))
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()
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
915
except PageEditor.EditConflict:
916
return LASTREV_INVALID
918
current_rev = currentpage.get_real_rev()
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)
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
940
@param pagename: pagename (utf-8)
942
@return: a list of utf-8 attachment names
944
pagename = self._instr(pagename)
945
# User may read page?
946
if not self.request.user.may.read(pagename):
947
return self.notAllowedFault()
949
result = AttachFile._get_files(self.request, pagename)
952
def xmlrpc_getAttachment(self, pagename, attachname):
953
""" Get attachname associated with pagename
955
@param pagename: pagename (utf-8)
956
@param attachname: attachment name (utf-8)
960
pagename = self._instr(pagename)
961
# User may read page?
962
if not self.request.user.may.read(pagename):
963
return self.notAllowedFault()
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())
971
def xmlrpc_putAttachment(self, pagename, attachname, data):
972
""" Set attachname associated with pagename to data
974
@param pagename: pagename (utf-8)
975
@param attachname: attachment name (utf-8)
976
@param data: file data (base64)
978
@return True if attachment was set
980
pagename = self._instr(pagename)
981
# User may read page?
982
if not self.request.user.may.read(pagename):
983
return self.notAllowedFault()
986
if not self.request.user.may.write(pagename):
987
return xmlrpclib.Fault(1, "You are not allowed to edit this page")
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)
997
# XXX END WARNING XXX
1000
def xmlrpc_getBotTranslations(self):
1001
""" Return translations to be used by notification bot
1003
@return: a dict (indexed by language) of dicts of translated strings (indexed by original ones)
1005
from MoinMoin.i18n import bot_translations
1006
return bot_translations(self.request)
1009
class XmlRpc1(XmlRpcBase):
1011
def __init__(self, request):
1012
XmlRpcBase.__init__(self, request)
1015
def _instr(self, text):
1016
""" Convert string we get from xmlrpc into internal representation
1018
@param text: quoted text (str or unicode object)
1022
return wikiutil.url_unquote(text) # config.charset must be utf-8
1024
def _outstr(self, text):
1025
""" Convert string from internal representation to xmlrpc
1027
@param text: unicode or string in config.charset
1029
@return: text encoded in utf-8 and quoted
1031
return wikiutil.url_quote(text) # config.charset must be utf-8
1034
class XmlRpc2(XmlRpcBase):
1036
def __init__(self, request):
1037
XmlRpcBase.__init__(self, request)
1040
def _instr(self, text):
1041
""" Convert string we get from xmlrpc into internal representation
1043
@param text: unicode or utf-8 string
1047
if not isinstance(text, unicode):
1048
text = unicode(text, 'utf-8')
1051
def _outstr(self, text):
1052
""" Convert string from internal representation to xmlrpc
1054
@param text: unicode or string in config.charset
1056
@return: text encoded in utf-8
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')
1065
def xmlrpc(request):
1066
XmlRpc1(request).process()
1069
def xmlrpc2(request):
1070
XmlRpc2(request).process()