17
17
when really necessary (like for transferring binary files like
18
18
attachments maybe).
20
@copyright: 2003-2009 MoinMoin:ThomasWaldmann,
21
2004-2006 MoinMoin:AlexanderSchremmer,
22
2007-2009 MoinMoin:ReimarBauer
20
@copyright: 2003-2008 MoinMoin:ThomasWaldmann,
21
2004-2006 MoinMoin:AlexanderSchremmer
23
22
@license: GNU GPL, see COPYING for details
25
24
from MoinMoin.util import pysupport
31
30
from MoinMoin import log
32
31
logging = log.getLogger(__name__)
34
from MoinMoin import auth, config, user, wikiutil
33
from MoinMoin import config, user, wikiutil
35
34
from MoinMoin.Page import Page
36
35
from MoinMoin.PageEditor import PageEditor
37
36
from MoinMoin.logfile import editlog
38
37
from MoinMoin.action import AttachFile
39
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
42
54
logging_tearline = '- XMLRPC %s ' + '-' * 40
46
XMLRPC base class with common functionality of wiki xmlrpc v1 and v2
57
""" XMLRPC base class with common functionality of wiki xmlrpc v1 and v2 """
48
58
def __init__(self, request):
50
60
Initialize an XmlRpcBase object.
79
87
raise NotImplementedError("please implement _outstr in derived class")
81
89
def _inlob(self, text):
83
Convert inbound base64-encoded utf-8 to Large OBject.
90
""" Convert inbound base64-encoded utf-8 to Large OBject.
85
92
@param text: the text to convert
89
text = text.data # this is a already base64-decoded 8bit string
96
text = text.data #this is a already base64-decoded 8bit string
90
97
text = unicode(text, 'utf-8')
93
100
def _outlob(self, text):
95
Convert outbound Large OBject to base64-encoded utf-8.
101
""" Convert outbound Large OBject to base64-encoded utf-8.
97
103
@param text: the text, either unicode or utf-8 string
123
128
def process(self):
125
xmlrpc v1 and v2 dispatcher
127
request = self.request
129
""" xmlrpc v1 and v2 dispatcher """
129
131
if 'xmlrpc' in self.request.cfg.actions_excluded:
130
132
# we do not handle xmlrpc v1 and v2 differently
131
133
response = xmlrpclib.Fault(1, "This moin wiki does not allow xmlrpc method calls.")
133
# overwrite any user there might be, if you need a valid user for
134
# xmlrpc, you have to use multicall and getAuthToken / applyAuthToken
135
request.user = user.User(request, auth_method='xmlrpc:invalid')
137
data = request.read()
135
data = self.request.read(self.request.content_length)
140
138
params, method = xmlrpclib.loads(data)
169
167
response = xmlrpclib.dumps(response, methodresponse=1, allow_none=True)
171
request = request.request
172
request.content_type = 'text/xml'
173
request.data = response
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)
176
175
def dispatch(self, method, params):
178
call dispatcher - for method==xxx it either locates a method called
179
xmlrpc_xxx or loads a plugin from plugin/xmlrpc/xxx.py
176
""" call dispatcher - for method==xxx it either locates a method called
177
xmlrpc_xxx or loads a plugin from plugin/xmlrpc/xxx.py
181
179
method = method.replace(".", "_")
212
210
#############################################################################
214
212
def xmlrpc_system_multicall(self, call_list):
216
system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => [[4], ...]
213
"""system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => [[4], ...]
218
215
Allows the caller to package multiple XML-RPC calls into a single
262
258
return self.version
264
260
def xmlrpc_getAllPages(self):
266
Get all pages readable by current user
261
""" Get all pages readable by current user
269
264
@return: a list of all pages.
272
# the official WikiRPC interface is implemented by the extended method as well
267
# the official WikiRPC interface is implemented by the extended method
273
269
return self.xmlrpc_getAllPagesEx()
276
272
def xmlrpc_getAllPagesEx(self, opts=None):
278
Get all pages readable by current user. Not an WikiRPC method.
273
""" Get all pages readable by current user. Not an WikiRPC method.
280
275
@param opts: dictionary that can contain the following arguments:
281
276
include_system:: set it to false if you do not want to see system pages
384
378
return return_items
386
380
def xmlrpc_getPageInfo(self, pagename):
388
Invoke xmlrpc_getPageInfoVersion with rev=None
381
""" Invoke xmlrpc_getPageInfoVersion with rev=None """
390
382
return self.xmlrpc_getPageInfoVersion(pagename, rev=None)
392
384
def xmlrpc_getPageInfoVersion(self, pagename, rev):
394
Return page information for specific revision
385
""" Return page information for specific revision
396
387
@param pagename: the name of the page (utf-8)
397
388
@param rev: revision to get info about (int)
450
441
def xmlrpc_getPage(self, pagename):
452
Invoke xmlrpc_getPageVersion with rev=None
442
""" Invoke xmlrpc_getPageVersion with rev=None """
454
443
return self.xmlrpc_getPageVersion(pagename, rev=None)
456
445
def xmlrpc_getPageVersion(self, pagename, rev):
458
Get raw text from specific revision of pagename
446
""" Get raw text from specific revision of pagename
460
448
@param pagename: pagename (utf-8)
461
449
@param rev: revision number (int)
484
472
return self._outlob(page.get_raw_body())
486
474
def xmlrpc_getPageHTML(self, pagename):
488
Invoke xmlrpc_getPageHTMLVersion with rev=None
475
""" Invoke xmlrpc_getPageHTMLVersion with rev=None """
490
476
return self.xmlrpc_getPageHTMLVersion(pagename, rev=None)
492
478
def xmlrpc_getPageHTMLVersion(self, pagename, rev):
494
Get HTML of from specific revision of pagename
479
""" Get HTML of from specific revision of pagename
496
481
@param pagename: the page name (utf-8)
497
482
@param rev: revision number (int)
584
569
return xmlrpclib.Boolean(1)
586
def xmlrpc_renamePage(self, pagename, newpagename):
588
Renames a page <pagename> to <newpagename>.
590
@param pagename: the page name (unicode or utf-8)
591
@param newpagename: the new pagename (unicode or utf-8)
593
@return: True on success
596
pagename = self._instr(pagename)
597
pagename = wikiutil.normalize_pagename(pagename, self.cfg)
599
return xmlrpclib.Fault("INVALID", "pagename can't be empty")
602
if not (self.request.user.may.delete(pagename) and self.request.user.may.write(newpagename)):
603
return xmlrpclib.Fault(1, "You are not allowed to rename this page")
604
editor = PageEditor(self.request, pagename, do_editor_backup=0)
607
editor.renamePage(newpagename)
608
except PageEditor.SaveError, error:
609
return xmlrpclib.Fault(1, "Rename failed: %s" % (str(error), ))
611
return xmlrpclib.Boolean(1)
613
571
def xmlrpc_revertPage(self, pagename, revision):
615
Revert a page to previous revision
572
"""Revert a page to previous revision
617
574
This is mainly intended to be used by the jabber bot.
619
576
@param pagename: the page name (unicode or utf-8)
620
577
@param revision: revision to revert to
622
@return: True on success
579
@return true on success
626
pagename = self._instr(pagename)
628
582
if not self.request.user.may.write(pagename):
629
583
return xmlrpclib.Fault(1, "You are not allowed to edit this page")
631
585
rev = int(self._instr(revision))
632
editor = PageEditor(self.request, pagename, do_editor_backup=0)
586
editor = PageEditor(self.request, pagename)
635
589
editor.revertPage(rev)
639
593
return xmlrpclib.Boolean(1)
641
595
def xmlrpc_searchPages(self, query_string):
643
Searches pages for query_string.
644
Returns a list of tuples (foundpagename, context)
596
""" Searches pages for query_string.
597
Returns a list of tuples (foundpagename, context)
646
599
from MoinMoin import search
647
600
results = search.searchPages(self.request, query_string)
682
635
for hit in results.hits]
684
637
def xmlrpc_getMoinVersion(self):
686
Returns a tuple of the MoinMoin version:
687
(project, release, revision)
638
""" Returns a tuple of the MoinMoin version:
639
(project, release, revision)
689
641
from MoinMoin import version
690
642
return (version.project, version.release, version.revision)
693
645
# user profile data transfer
695
647
def xmlrpc_getUserProfile(self):
697
Return the user profile data for the current user.
698
Use this in a single multicall after applyAuthToken.
699
If we have a valid user, returns a dict of items from user profile.
700
Otherwise, return an empty dict.
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.
702
653
u = self.request.user
726
676
# authorization methods
728
678
def xmlrpc_getAuthToken(self, username, password, *args):
730
Returns a token which can be used for authentication
731
in other XMLRPC calls. If the token is empty, the username
732
or the password were wrong.
734
Implementation note: token is same as cookie content would be for http session
736
request = self.request
737
request.session = request.cfg.session_service.get_session(request)
739
u = auth.setup_from_session(request, request.session)
740
u = auth.handle_login(request, u, username=username, password=password)
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)
742
691
if u and u.valid:
744
request.cfg.session_service.finalize(request, request.session)
745
return request.session.sid
692
return id_handler.token
749
696
def xmlrpc_getJabberAuthToken(self, jid, secret):
751
Returns a token which can be used for authentication.
697
"""Returns a token which can be used for authentication.
753
699
This token can be used in other XMLRPC calls. Generation of
754
700
token depends on user's JID and a secret shared between wiki
757
703
@param jid: a bare Jabber ID
759
if self.cfg.secrets['jabberbot'] != secret:
760
logging.warning("getJabberAuthToken: got wrong secret %r" % secret)
705
if self.cfg.secret != secret:
763
request = self.request
764
request.session = request.cfg.session_service.get_session(request)
765
logging.debug("getJabberAuthToken: got session %r" % request.session)
767
u = user.get_by_jabber_id(request, jid) # XXX is someone talking to use from a jid we have stored in
768
# XXX some user profile enough to authenticate him as that user?
769
logging.debug("getJabberAuthToken: got user %r" % u)
708
u = self.request.handle_jid_auth(jid)
771
710
if u and u.valid:
772
u.auth_method = 'moin' # XXX fake 'moin' login so the check for known login methods succeeds
773
# XXX if not patched, u.auth_method is 'internal', but that is not accepted either
774
# TODO this should be done more cleanly, somehow
776
request.cfg.session_service.finalize(request, request.session)
777
logging.debug("getJabberAuthToken: returning sid %r" % request.session.sid)
778
return request.session.sid
711
return self._generate_auth_token(u)
782
715
def xmlrpc_applyAuthToken(self, auth_token):
784
Applies the auth token and thereby authenticates the user.
716
""" Applies the auth token and thereby authenticates the user. """
786
717
if not auth_token:
787
718
return xmlrpclib.Fault("INVALID", "Empty token.")
789
request = self.request
790
request.session = request.cfg.session_service.get_session(request, auth_token)
791
logging.debug("applyAuthToken: got session %r" % request.session)
792
u = auth.setup_from_session(request, request.session)
793
logging.debug("applyAuthToken: got user %r" % u)
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)
795
725
if u and u.valid:
796
726
self.request.user = u
802
732
def xmlrpc_deleteAuthToken(self, auth_token):
804
Delete the given auth token.
807
return xmlrpclib.Fault("INVALID", "Empty token.")
809
request = self.request
810
request.session = request.cfg.session_service.get_session(request, auth_token)
811
request.cfg.session_service.destroy_session(request, request.session)
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()
816
745
# methods for wiki synchronization
818
747
def xmlrpc_getDiff(self, pagename, from_rev, to_rev, n_name=None):
820
Gets the binary difference between two page revisions.
822
@param pagename: unicode string qualifying the page name
824
@param fromRev: integer specifying the source revision. May be None to
825
refer to a virtual empty revision which leads to a diff
826
containing the whole page.
828
@param toRev: integer specifying the target revision. May be None to
829
refer to the current revision. If the current revision is the same
830
as fromRev, there will be a special error condition "ALREADY_CURRENT"
832
@param n_name: do a tag check verifying that n_name was the normalised
835
If both fromRev and toRev are None, this function acts similar to getPage, i.e. it will diff("",currentRev).
837
@return: Returns a dict:
838
* status (not a field, implicit, returned as Fault if not SUCCESS):
839
* "SUCCESS" - if the diff could be retrieved successfully
840
* "NOT_EXIST" - item does not exist
841
* "FROMREV_INVALID" - the source revision is invalid
842
* "TOREV_INVALID" - the target revision is invalid
843
* "INTERNAL_ERROR" - there was an internal error
844
* "INVALID_TAG" - the last tag does not match the supplied normalised name
845
* "ALREADY_CURRENT" - this not merely an error condition. It rather means that
846
there is no new revision to diff against which is a good thing while
848
* current: the revision number of the current revision (not the one which was diff'ed against)
849
* diff: Binary object that transports a zlib-compressed binary diff (see bdiff.py, taken from Mercurial)
850
* conflict: if there is a conflict on the page currently
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
852
781
from MoinMoin.util.bdiff import textdiff, compress
853
782
from MoinMoin.wikisync import TagStore
923
850
return [self._outstr(name), iwid]
925
852
def xmlrpc_mergeDiff(self, pagename, diff, local_rev, delta_remote_rev, last_remote_rev, interwiki_name, normalised_name):
927
Merges a diff sent by the remote machine and returns the number of the new revision.
928
Additionally, this method tags the new revision.
930
@param pagename: The pagename that is currently dealt with.
931
@param diff: The diff that can be applied to the version specified by delta_remote_rev.
932
If it is None, the page is deleted.
933
@param local_rev: The revno of the page on the other wiki system, used for the tag.
934
@param delta_remote_rev: The revno that the diff is taken against.
935
@param last_remote_rev: The last revno of the page `pagename` that is known by the other wiki site.
936
@param interwiki_name: Used to build the interwiki tag.
937
@param normalised_name: The normalised pagename that is common to both wikis.
939
@return: Returns the current revision number after the merge was done. Or one of the following errors:
940
* "SUCCESS" - the page could be merged and tagged successfully.
941
* "NOT_EXIST" - item does not exist and there was not any content supplied.
942
* "LASTREV_INVALID" - the page was changed and the revision got invalid
943
* "INTERNAL_ERROR" - there was an internal error
944
* "NOT_ALLOWED" - you are not allowed to do the merge operation on the page
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
946
872
from MoinMoin.util.bdiff import decompress, patch
947
873
from MoinMoin.wikisync import TagStore, BOTH
1027
952
def xmlrpc_getAttachment(self, pagename, attachname):
1029
Get contents of attachment <attachname> of page <pagename>
953
""" Get attachname associated with pagename
1031
955
@param pagename: pagename (utf-8)
1032
956
@param attachname: attachment name (utf-8)
1034
@return: base64 data
1036
960
pagename = self._instr(pagename)
1037
961
# User may read page?
1038
962
if not self.request.user.may.read(pagename):
1039
963
return self.notAllowedFault()
1041
attachname = wikiutil.taintfilename(self._instr(attachname))
1042
filename = AttachFile.getFilename(self.request, pagename, attachname)
965
filename = wikiutil.taintfilename(self._instr(attachname))
966
filename = AttachFile.getFilename(self.request, pagename, filename)
1043
967
if not os.path.isfile(filename):
1044
968
return self.noSuchPageFault()
1045
969
return self._outlob(open(filename, 'rb').read())
1047
971
def xmlrpc_putAttachment(self, pagename, attachname, data):
1049
Store <data> as content of attachment <attachname> of page <pagename>.
972
""" Set attachname associated with pagename to data
1051
974
@param pagename: pagename (utf-8)
1052
975
@param attachname: attachment name (utf-8)
1053
976
@param data: file data (base64)
1055
@return: True if attachment was successfully stored
978
@return True if attachment was set
1057
980
pagename = self._instr(pagename)
1058
981
# User may read page?
1063
986
if not self.request.user.may.write(pagename):
1064
987
return xmlrpclib.Fault(1, "You are not allowed to edit this page")
1066
attachname = wikiutil.taintfilename(self._instr(attachname))
989
attachname = wikiutil.taintfilename(attachname)
1067
990
filename = AttachFile.getFilename(self.request, pagename, attachname)
1068
991
if os.path.exists(filename) and not os.path.isfile(filename):
1069
992
return self.noSuchPageFault()
1070
993
open(filename, 'wb+').write(data.data)
1071
AttachFile._addLogEntry(self.request, 'ATTNEW', pagename, attachname)
1072
return xmlrpclib.Boolean(1)
1074
def xmlrpc_deleteAttachment(self, pagename, attachname):
1076
Deletes attachment <attachname> of page <pagename>.
1078
@param pagename: pagename (utf-8)
1079
@param attachname: attachment name (utf-8)
1081
@return: True on success
1083
pagename = self._instr(pagename)
1085
if not self.request.user.may.delete(pagename):
1086
return xmlrpclib.Fault(1, 'You are not allowed to delete attachments on this page.')
1088
attachname = wikiutil.taintfilename(self._instr(attachname))
1089
filename = AttachFile.getFilename(self.request, pagename, attachname)
1090
AttachFile.remove_attachment(self.request, pagename, attachname)
994
AttachFile._addLogEntry(self.request, 'ATTNEW', pagename, filename)
1091
995
return xmlrpclib.Boolean(1)
1093
997
# XXX END WARNING XXX
1095
1000
def xmlrpc_getBotTranslations(self):
1097
Return translations to be used by notification bot
1001
""" Return translations to be used by notification bot
1099
1003
@return: a dict (indexed by language) of dicts of translated strings (indexed by original ones)