changeset 985:eaae4bcf60f3

merged main
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Tue, 18 Jul 2006 11:48:53 +0200
parents 311492a91530 (current diff) f56db9746839 (diff)
children 175695a510f5 65dc979761f0
files
diffstat 8 files changed, 383 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/Page.py	Tue Jul 18 11:34:23 2006 +0200
+++ b/MoinMoin/Page.py	Tue Jul 18 11:48:53 2006 +0200
@@ -780,7 +780,7 @@
     def get_raw_body(self):
         """ Load the raw markup from the page file.
 
-        @rtype: str
+        @rtype: unicode
         @return: raw page contents of this page
         """
         if self._raw_body is None:
@@ -806,7 +806,15 @@
                 file.close()
 
         return self._raw_body
+    
+    def get_raw_body_str(self):
+        """ Returns the raw markup from the page file, as a string.
 
+        @rtype: str
+        @return: raw page contents of this page
+        """
+        return self.get_raw_body().encode("utf-8")
+    
     def set_raw_body(self, body, modified=0):
         """ Set the raw body text (prevents loading from disk).
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/SyncPages.py	Tue Jul 18 11:48:53 2006 +0200
@@ -0,0 +1,98 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - SyncPages action
+
+    This action allows you to synchronise pages of two wikis.
+
+    @copyright: 2006 MoinMoin:AlexanderSchremmer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import os
+import zipfile
+import xmlrpclib
+from datetime import datetime
+
+from MoinMoin import wikiutil, config, user
+from MoinMoin.PageEditor import PageEditor
+from MoinMoin.Page import Page
+from MoinMoin.wikidicts import Dict
+
+class ActionStatus(Exception): pass
+
+class RemoteWiki(object):
+    """ This class should be the base for all implementations of remote wiki
+        classes. """
+    def getInterwikiName(self):
+        """ Returns the interwiki name of the other wiki. """
+        return NotImplemented
+    
+    def __repr__(self):
+        """ Returns a representation of the instance for debugging purposes. """
+        return NotImplemented
+
+class MoinWiki(RemoteWiki):
+    def __init__(self, interwikiname):
+        wikitag, wikiurl, wikitail, wikitag_bad = wikiutil.resolve_wiki(self.request, '%s:""' % (interwikiname, ))
+        self.wiki_url = wikiutil.mapURL(self.request, wikiurl)
+        self.valid = not wikitag_bad
+        self.xmlrpc_url = self.wiki_url + "?action=xmlrpc2"
+        self.connection = self.createConnection()
+
+    def createConnection(self):
+        return xmlrpclib.ServerProxy(self.xmlrpc_url, allow_none=True)
+
+    # Methods implementing the RemoteWiki interface
+    def getInterwikiName(self):
+        return self.connection.interwikiName()
+
+    def __repr__(self):
+        return "<RemoteWiki wiki_url=%r valid=%r>" % (self.valid, self.wiki_url)
+
+class ActionClass:
+    def __init__(self, pagename, request):
+        self.request = request
+        self.pagename = pagename
+        self.page = Page(request, pagename)
+
+    def parsePage(self):
+        defaults = {
+            "remotePrefix": "",
+            "localPrefix": "",
+            "remoteWiki": ""
+        }
+        
+        defaults.update(Dict(self.request, self.pagename).get_dict())
+        return defaults
+        
+    def render(self):
+        """ Render action
+
+        This action returns a wiki page with optional message, or
+        redirects to new page.
+        """
+        _ = self.request.getText
+        
+        params = self.parsePage()
+        
+        try:
+            if not self.request.cfg.interwikiname:
+                raise ActionStatus(_("Please set an interwikiname in your wikiconfig (see HelpOnConfiguration) to be able to use this action."))
+
+            if not params["remoteWiki"]:
+                raise ActionStatus(_("Incorrect parameters. Please supply at least the ''remoteWiki'' parameter."))
+            
+            remote = MoinWiki(params["remoteWiki"])
+            
+            if not remote.valid:
+                raise ActionStatus(_("The ''remoteWiki'' is unknown."))
+            
+            # ...
+            self.sync(params)
+        except ActionStatus, e:
+            return self.page.send_page(self.request, msg=u'<p class="error">%s</p>\n' % (e.args[0], ))
+
+        return self.page.send_page(self.request, msg=_("Syncronisation finished."))
+    
+def execute(pagename, request):
+    ActionClass(pagename, request).render()
--- a/MoinMoin/action/__init__.py	Tue Jul 18 11:34:23 2006 +0200
+++ b/MoinMoin/action/__init__.py	Tue Jul 18 11:48:53 2006 +0200
@@ -20,7 +20,7 @@
     actions_excluded, making and checking tickets, rendering some form,
     displaying errors and doing stuff after an action.
     
-    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>,
+    @copyright: 2000-2004 by Jrgen Hermann <jh@web.de>,
                 2006 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
@@ -427,7 +427,7 @@
     # Save new text
     else:
         try:
-            still_conflict = r"/!\ '''Edit conflict" in savetext
+            still_conflict = wikiutil.containsConflictMarker(savetext)
             pg.setConflict(still_conflict)
             savemsg = pg.saveText(savetext, rev, trivial=trivial, comment=comment)
         except pg.EditConflict, e:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/bdiff.py	Tue Jul 18 11:48:53 2006 +0200
@@ -0,0 +1,93 @@
+"""
+    MoinMoin - Binary patching and diffing
+
+    @copyright: 2005 Matt Mackall <mpm@selenic.com>
+    @copyright: 2006 MoinMoin:AlexanderSchremmer
+    
+    Algorithm taken from mercurial's mdiff.py
+    
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import zlib, difflib, struct
+
+BDIFF_PATT = ">lll"
+BDIFF_PATT_SIZE = struct.calcsize(BDIFF_PATT)
+
+def compress(text):
+    return zlib.compress(text) # here we could tune the compression level
+
+def decompress(bin):
+    return zlib.decompress(bin)
+
+def diff(a, b):
+    """ Generates a binary diff of the passed strings.
+        Note that you can pass arrays of strings as well.
+        This might give you better results for text files. """
+    if not a:
+        s = "".join(b)
+        return s and (struct.pack(BDIFF_PATT, 0, 0, len(s)) + s)
+
+    bin = []
+    la = lb = 0
+    
+    p = [0]
+    for i in a: p.append(p[-1] + len(i))
+    
+    for am, bm, size in difflib.SequenceMatcher(None, a, b).get_matching_blocks():
+        s = "".join(b[lb:bm])
+        if am > la or s:
+            bin.append(struct.pack(BDIFF_PATT, p[la], p[am], len(s)) + s)
+        la = am + size
+        lb = bm + size
+    
+    return "".join(bin)
+
+def textdiff(a, b):
+    """ A diff function optimised for text files. Works with binary files as well. """
+    return diff(a.splitlines(1), b.splitlines(1))
+
+def patchtext(bin):
+    """ Returns the new hunks that are contained in a binary diff."""
+    pos = 0
+    t = []
+    while pos < len(bin):
+        p1, p2, l = struct.unpack(BDIFF_PATT, bin[pos:pos + BDIFF_PATT_SIZE])
+        pos += BDIFF_PATT_SIZE
+        t.append(bin[pos:pos + l])
+        pos += l
+    return "".join(t)
+
+def patch(a, bin):
+    """ Patches the string a with the binary patch bin. """
+    c = last = pos = 0
+    r = []
+
+    while pos < len(bin):
+        p1, p2, l = struct.unpack(BDIFF_PATT, bin[pos:pos + BDIFF_PATT_SIZE])
+        pos += BDIFF_PATT_SIZE
+        r.append(a[last:p1])
+        r.append(bin[pos:pos + l])
+        pos += l
+        last = p2
+        c += 1
+    r.append(a[last:])
+
+    return "".join(r)
+
+def test():
+    a = ("foo\n" * 30)
+    b = ("  fao" * 30)
+    
+    a = file(r"C:\Dokumente und Einstellungen\Administrator\Eigene Dateien\Progra\Python\MoinMoin\moin-1.6-sync\MoinMoin\util\test.1").read()
+    b = file(r"C:\Dokumente und Einstellungen\Administrator\Eigene Dateien\Progra\Python\MoinMoin\moin-1.6-sync\MoinMoin\util\test.2").read()
+    a = a.splitlines(1)
+    b = b.splitlines(1)
+    
+    d = diff(a, b)
+    z = compress(d)
+    print `patchtext(d)`
+    print `d`
+    print "".join(b) == patch("".join(a), d)
+    print len(d), len(z)
+
--- a/MoinMoin/wikidicts.py	Tue Jul 18 11:34:23 2006 +0200
+++ b/MoinMoin/wikidicts.py	Tue Jul 18 11:48:53 2006 +0200
@@ -59,6 +59,9 @@
 
     def values(self):
         return self._dict.values()
+    
+    def get_dict(self):
+        return self._dict
 
     def has_key(self, key):
         return self._dict.has_key(key)
--- a/MoinMoin/wikiutil.py	Tue Jul 18 11:34:23 2006 +0200
+++ b/MoinMoin/wikiutil.py	Tue Jul 18 11:48:53 2006 +0200
@@ -2,7 +2,7 @@
 """
     MoinMoin - Wiki Utility Functions
 
-    @copyright: 2000 - 2004 by Jürgen Hermann <jh@web.de>
+    @copyright: 2000 - 2004 by Jrgen Hermann <jh@web.de>
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -1445,6 +1445,9 @@
     else:
         return "%s%s</a>" % (result, text)
 
+def containsConflictMarker(text):
+    """ Returns true if there is a conflict marker in the text. """
+    return "/!\ '''Edit conflict" in text
 
 def linediff(oldlines, newlines, **kw):
     """
--- a/MoinMoin/xmlrpc/__init__.py	Tue Jul 18 11:34:23 2006 +0200
+++ b/MoinMoin/xmlrpc/__init__.py	Tue Jul 18 11:48:53 2006 +0200
@@ -492,18 +492,151 @@
                 for hit in results.hits]
 
     def xmlrpc_getMoinVersion(self):
+        """ Returns a tuple of the MoinMoin version:
+            (project, release, revision)
+        """
         from MoinMoin import version
         return (version.project, version.release, version.revision)
 
+    # authorization methods
+    
+    def xmlrpc_getAuthToken(self, username, password, *args):
+        """ Returns a token which can be used for authentication
+            in other XMLRPC calls. If the token is empty, the username
+            or the password were wrong. """
+        u = user.User(self.request, name=username, password=password, auth_method='xmlrpc_gettoken')
+        if u.valid:
+            return u.id
+        else:
+            return ""
+    
+    def xmlrpc_applyAuthToken(self, auth_token):
+        """ Applies the auth token and thereby authenticates the user. """
+        u = user.User(self.request, id=auth_token, auth_method='xmlrpc_applytoken')
+        if u.valid:
+            self.request.user = u
+            return "SUCCESS"
+        else:
+            return xmlrpclib.Fault("INVALID", "Invalid token.")
+    
+    def xmlrpc_getDiff(self, pagename, from_rev, to_rev):
+        """ Gets the binary difference between two page revisions. See MoinMoin:WikiSyncronisation. """
+        from MoinMoin.util.bdiff import textdiff, compress
+        
+        pagename = self._instr(pagename)
 
+        # User may read page?
+        if not self.request.user.may.read(pagename):
+            return self.notAllowedFault()
+
+        def allowed_rev_type(data):
+            if data is None:
+                return True
+            return isinstance(data, int) and data > 0
+
+        if not allowed_rev_type(from_rev):
+            return xmlrpclib.Fault("FROMREV_INVALID", "Incorrect type for from_rev.")
+        
+        if not allowed_rev_type(to_rev):
+            return xmlrpclib.Fault("TOREV_INVALID", "Incorrect type for to_rev.")
+        
+        currentpage = Page(self.request, pagename)
+        if not currentpage.exists():
+            return xmlrpclib.Fault("NOT_EXIST", "Page does not exist.")
+        
+        revisions = currentpage.getRevList()
+        
+        if from_rev is not None and from_rev not in revisions:
+            return xmlrpclib.Fault("FROMREV_INVALID", "Unknown from_rev.")
+        if to_rev is not None and to_rev not in revisions:
+            return xmlrpclib.Fault("TOREV_INVALID", "Unknown to_rev.")
+        
+        # use lambda to defer execution in the next lines
+        if from_rev is None:
+            oldcontents = lambda: ""
+        else:
+            oldpage = Page(request, pagename, rev=from_rev)
+            oldcontents = lambda: oldpage.get_raw_body_str()
+        
+        if to_rev is None:
+            newcontents = lambda: currentpage.get_raw_body()
+        else:
+            newpage = Page(request, pagename, rev=to_rev)
+            newcontents = lambda: newpage.get_raw_body_str()
+            newrev = newpage.get_real_rev()
+        
+        if oldcontents() and oldpage.get_real_rev() == newpage.get_real_rev():
+            return xmlrpclib.Fault("ALREADY_CURRENT", "There are no changes.")
+        
+        newcontents = newcontents()
+        conflict = wikiutil.containsConflictMarker(newcontents)
+        diffblob = xmlrpclib.Binary(compress(textdiff(oldcontents(), newcontents)))
+        
+        return {"conflict": conflict, "diff": diffblob, "diffversion": 1, "current": currentpage.get_real_rev()}
+    
+    def xmlrpc_interwikiName(self):
+        """ Returns the interwiki name of the current wiki. """
+        name = self.request.cfg.interwikiname
+        if name is None:
+            return None
+        else:
+            return self._outstr(name)
+    
+    def xmlrpc_mergeChanges(self, pagename, diff, local_rev, delta_remote_rev, last_remote_rev, interwiki_name):
+        """ Merges a diff sent by the remote machine and returns the number of the new revision.
+            Additionally, this method tags the new revision.
+            
+            @param pagename: The pagename that is currently dealt with.
+            @param diff: The diff that can be applied to the version specified by delta_remote_rev.
+            @param local_rev: The revno of the page on the other wiki system, used for the tag.
+            @param delta_remote_rev: The revno that the diff is taken against.
+            @param last_remote_rev: The last revno of the page `pagename` that is known by the other wiki site.
+            @param interwiki_name: Used to build the interwiki tag.
+        """
+        from MoinMoin.util.bdiff import decompress, patch
+        
+        pagename = self._instr(pagename)
+       
+        # User may read page?
+        if not self.request.user.may.read(pagename) or not self.request.user.may.write(pagename):
+            return self.notAllowedFault()
+
+        # XXX add locking here!
+        
+        # current version of the page
+        currentpage = Page(self.request, pagename)
+
+        if currentpage.get_real_rev() != last_remote_rev:
+            return xmlrpclib.Fault("LASTREV_INVALID", "The page was changed")
+        
+        if not currentpage.exists() and diff is None:
+            return xmlrpclib.Fault("NOT_EXIST", "The page does not exist and no diff was supplied.")
+        
+        # base revision used for the diff
+        basepage = Page(self.request, pagename, rev=delta_remote_rev)
+        
+        # generate the new page revision by applying the diff
+        newcontents = patch(basepage.get_raw_body_str(), decompress(str(diff)))
+        
+        # write page
+        # XXX ...
+        
+        # XXX add a tag (interwiki_name, local_rev, current rev) to the page
+        # XXX return current rev
+        # XXX finished
+        
+        
     # XXX BEGIN WARNING XXX
     # All xmlrpc_*Attachment* functions have to be considered as UNSTABLE API -
     # they are neither standard nor are they what we need when we have switched
     # attachments (1.5 style) to mimetype items (hopefully in 1.6).
-    # They are likely to get removed again when we remove AttachFile module.
-    # So use them on your own risk.
+    # They will be partly removed, esp. the semantics of the function "listAttachments"
+    # cannot be sensibly defined for items.
+    # If the first beta or more stable release of 1.6 will have new item semantics,
+    # we will remove the functions before it is released.
     def xmlrpc_listAttachments(self, pagename):
         """ Get all attachments associated with pagename
+        Deprecated.
         
         @param pagename: pagename (utf-8)
         @rtype: list
--- a/docs/CHANGES.aschremmer	Tue Jul 18 11:34:23 2006 +0200
+++ b/docs/CHANGES.aschremmer	Tue Jul 18 11:48:53 2006 +0200
@@ -6,27 +6,61 @@
 
   ToDo:
     * Implement actual syncronisation.
-    * Implement a cross-site authentication system.
+    * Implement a cross-site authentication system, i.e. mainly an
+      identity storage.
+    * Clean up trailing whitespace.
+    * Add page locking.
+    * How about using unique IDs that just derive from the interwikiname?
+    * How to handle renames?
 
   New Features:
     * XMLRPC method to return the Moin version
     * XMLRPC multicall support
     * Conflict icon in RecentChanges
+    * XMLRPC Authentication System
+    * Binary Diffing
+    * XMLRPC method to get binary diffs
+    * 
 
   Bugfixes (only stuff that is buggy in moin/1.6 main branch):
-    * ...
+    * Conflict resolution fixes.
 
   Other Changes:
     * Refactored conflict resolution and XMLRPC code.
+    * Enhanced API at some points.
 
   Developer notes:
     * ...
 
+Do not forget to check the related wiki page: http://moinmoin.wikiwikiweb.de/WikiSyncronisation
 
 Diary
 =====
 
-Week 21: Basic Infrastructur setup (repos), initial talks to the mentor, started writing the design document, helped other students to get started, started evaluating Mercurial as a DVCS backend
-Week 22: Tax forms, Fulfilled transcription request, written conflict icon support, refactored conflict handling, changed conflict icon, Added xmlrpc multicall support into the server and backported the client code from python 2.4
+Week 21: Basic Infrastructur setup (repos),
+         initial talks to the mentor, started writing the design document,
+         helped other students to get started
+Week 22: Tax forms, Fulfilled transcription request,
+         written conflict icon support, refactored conflict handling,
+         changed conflict icon,
+         Added xmlrpc multicall support into the server and
+         backported the client code from python 2.4
+Week 23: Debian-Sprint in Extremadura, Spain. Initial thoughts about Mercurial as
+         a base for syncronisation. (See wiki)
+Week 24: Evaluation of OpenID as a base for authentication, written local testing scripts
+Week 25: Conference in Chile (FET 2006).
+Week 26: Implementation of the XMLRPC authentication system, added binary
+         diffing (mainly taken from Mercurial, but had to merge 5 changesets,
+         remove some mercurial dependencies and document it. Currently, Mercurial
+         uses a module written in C to solve the problem, so the Python code
+         was not cared for anymore.)
+Week 27: Europython, Geneva.
+Week 28: Debian-Edu Developer Camp. Implemented getDiff XMLRPC method, added preliminary SyncPages action,
+         added interwikiName XMLRPC method, added mergeChanges XMLRPC method. Started analysis of the moinupdate
+         script written by Stefan Merten.
 
-
+Time plan
+=========
+In July and August, most parts of the implementation will be finished
+from 07-10 to 07-14 and from 08-03 to 08-19. Between those time spans, there
+are exams.