changeset 1210:a31940162a32

merge with main
author Franz Pletz <fpletz AT franz-pletz DOT org>
date Mon, 31 Jul 2006 12:24:50 +0200
parents 017deaab4afd (current diff) 59f77f472b06 (diff)
children d028d37e7105
files MoinMoin/action/fullsearch.py MoinMoin/config/multiconfig.py
diffstat 63 files changed, 1068 insertions(+), 586 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sat Jul 29 23:19:55 2006 +0200
+++ b/Makefile	Mon Jul 31 12:24:50 2006 +0200
@@ -33,7 +33,7 @@
 
 # Create documentation
 epydoc: patchlevel
-	@epydoc -o ../html -n MoinMoin -u http://moinmoin.wikiwikiweb.de MoinMoin
+	@epydoc -o ../html-1.6 -n MoinMoin -u http://moinmoin.wikiwikiweb.de MoinMoin
 
 # Create new underlay directory from MoinMaster
 # Should be used only on TW machine
--- a/MoinMoin/Page.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/Page.py	Mon Jul 31 12:24:50 2006 +0200
@@ -964,22 +964,21 @@
     def send_raw(self):
         """ Output the raw page data (action=raw) """
         request = self.request
-        request.http_headers(["Content-type: text/plain;charset=%s" % config.charset])
+        request.setHttpHeader("Content-type: text/plain; charset=%s" % config.charset)
         if self.exists():
             # use the correct last-modified value from the on-disk file
             # to ensure cacheability where supported. Because we are sending
             # RAW (file) content, the file mtime is correct as Last-Modified header.
-            request.http_headers(["Last-Modified: " +
-                 timefuncs.formathttpdate(os.path.getmtime(self._text_filename()))])
-
+            request.setHttpHeader("Status: 200 OK")
+            request.setHttpHeader("Last-Modified: %s" % timefuncs.formathttpdate(os.path.getmtime(self._text_filename())))
             text = self.get_raw_body()
             text = self.encodeTextMimeType(text)
-            request.write(text)
         else:
-            request.http_headers(['Status: 404 NOTFOUND'])
-            request.setResponseCode(404)
-            request.write(u"Page %s not found." % self.page_name)
+            request.setHttpHeader('Status: 404 NOTFOUND')
+            text = u"Page %s not found." % self.page_name
 
+        request.emit_http_headers()
+        request.write(text)
 
     def send_page(self, request, msg=None, **keywords):
         """ Output the formatted page.
@@ -990,7 +989,7 @@
 
         @param request: the request object
         @param msg: if given, display message in header area
-        @keyword content_only: if 1, omit page header and footer
+        @keyword content_only: if 1, omit http headers, page header and footer
         @keyword content_id: set the id of the enclosing div
         @keyword count_hit: if 1, add an event to the log
         @keyword send_missing_page: if 1, assume that page to be sent is MissingPage
@@ -1036,14 +1035,12 @@
                 except wikiutil.PluginMissingError:
                     pass
             else:
-                raise "Plugin missing error!" # XXX what now?
+                raise NotImplementedError("Plugin missing error!") # XXX what now?
         request.formatter = self.formatter
         self.formatter.setPage(self)
         if self.hilite_re:
             self.formatter.set_highlight_re(self.hilite_re)
         
-        request.http_headers(["Content-Type: %s; charset=%s" % (self.output_mimetype, self.output_charset)])
-
         # default is wiki markup
         pi_format = self.cfg.default_markup or "wiki"
         pi_formatargs = ''
@@ -1162,25 +1159,26 @@
         doc_leader = self.formatter.startDocument(self.page_name)
         page_exists = self.exists()
         if not content_only:
-            # send the document leader
-
-            # use "nocache" headers if we're using a method that
-            # is not simply "display", or if a user is logged in
-            # (which triggers personalisation features)
-
+            request.setHttpHeader("Content-Type: %s; charset=%s" % (self.output_mimetype, self.output_charset))
             if page_exists:
+                request.setHttpHeader('Status: 200 OK')
                 if not request.cacheable or request.user.valid:
-                    request.http_headers(request.nocache)
+                    # use "nocache" headers if we're using a method that
+                    # is not simply "display", or if a user is logged in
+                    # (which triggers personalisation features)
+                    for header in request.nocache:
+                        request.setHttpHeader(header)
                 else:
                     # TODO: we need to know if a page generates dynamic content
                     # if it does, we must not use the page file mtime as last modified value
                     # XXX The following code is commented because it is incorrect for dynamic pages:
                     #lastmod = os.path.getmtime(self._text_filename())
-                    #request.http_headers(["Last-Modified: %s" % timefuncs.formathttpdate(lastmod)])
-                    request.http_headers()
+                    #request.setHttpHeader("Last-Modified: %s" % timefuncs.formathttpdate(lastmod))
+                    pass
             else:
-                request.http_headers(['Status: 404 NOTFOUND'])
-                request.setResponseCode(404)
+                request.setHttpHeader('Status: 404 NOTFOUND')
+            request.emit_http_headers()
+
             request.write(doc_leader)
 
             # send the page header
@@ -1247,7 +1245,7 @@
             except wikiutil.PluginMissingError:
                 pass
         else:
-            raise "No matching parser" # XXX what do we use if nothing at all matches?
+            raise NotImplementedError("No matching parser") # XXX what do we use if nothing at all matches?
             
         # start wiki content div
         request.write(self.formatter.startContent(content_id))
@@ -1339,7 +1337,7 @@
                     except wikiutil.PluginMissingError:
                         pass
                 else:
-                    raise "no matching parser" # XXX what now?
+                    raise NotImplementedError("no matching parser") # XXX what now?
             return getattr(parser, 'caching', False)
         return False
 
@@ -1362,11 +1360,15 @@
             try:
                 code = self.loadCache(request)
                 self.execute(request, parser, code)
-            except 'CacheNeedsUpdate':
+            except Exception, (msg, ):
+                if msg != 'CacheNeedsUpdate':
+                    raise
                 try:
                     code = self.makeCache(request, parser)
                     self.execute(request, parser, code)
-                except 'CacheNeedsUpdate':
+                except Exception, (msg, ):
+                    if msg != 'CacheNeedsUpdate':
+                        raise
                     request.log('page cache failed after creation')
                     self.format(parser)
         
@@ -1385,26 +1387,32 @@
         import MoinMoin
         if hasattr(MoinMoin, '__loader__'):
             __file__ = os.path.join(MoinMoin.__loader__.archive, 'dummy')
-        exec code
+
+        try:
+            exec code
+        except "CacheNeedsUpdate": # convert the exception
+            raise Exception("CacheNeedsUpdate")
 
     def loadCache(self, request):
         """ Return page content cache or raises 'CacheNeedsUpdate' """
         cache = caching.CacheEntry(request, self, self.getFormatterName(), scope='item')
         attachmentsPath = self.getPagePath('attachments', check_create=0)
         if cache.needsUpdate(self._text_filename(), attachmentsPath):
-            raise 'CacheNeedsUpdate'
+            raise Exception('CacheNeedsUpdate')
         
         import marshal
         try:
             return marshal.loads(cache.content())
+        except "CacheNeedsUpdate": # convert old exception into a new one
+            raise Exception('CacheNeedsUpdate')
         except (EOFError, ValueError, TypeError):
             # Bad marshal data, must update the cache.
             # See http://docs.python.org/lib/module-marshal.html
-            raise 'CacheNeedsUpdate'
+            raise Exception('CacheNeedsUpdate')
         except Exception, err:
             request.log('fail to load "%s" cache: %s' % 
                         (self.page_name, str(err)))
-            raise 'CacheNeedsUpdate'
+            raise Exception('CacheNeedsUpdate')
 
     def makeCache(self, request, parser):
         """ Format content into code, update cache and return code """
--- a/MoinMoin/PageEditor.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/PageEditor.py	Mon Jul 31 12:24:50 2006 +0200
@@ -141,7 +141,7 @@
 
         form = self.request.form
         _ = self._
-        self.request.http_headers(self.request.nocache)
+        self.request.emit_http_headers(self.request.nocache)
 
         raw_body = ''
         msg = None
--- a/MoinMoin/PageGraphicalEditor.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/PageGraphicalEditor.py	Mon Jul 31 12:24:50 2006 +0200
@@ -54,7 +54,7 @@
         request = self.request
         form = self.request.form
         _ = self._
-        self.request.http_headers(self.request.nocache)
+        self.request.emit_http_headers(self.request.nocache)
 
         raw_body = ''
         msg = None
--- a/MoinMoin/__init__.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/__init__.py	Mon Jul 31 12:24:50 2006 +0200
@@ -1,6 +1,6 @@
 # -*- coding: iso-8859-1 -*-
 """
-MoinMoin Version 1.6.0alpha bf18e19e618d+ tip
+MoinMoin Version 1.6.0alpha 61142a50c41b+ tip
 
 @copyright: 2000-2006 by Jürgen Hermann <jh@web.de>
 @license: GNU GPL, see COPYING for details.
--- a/MoinMoin/_tests/test_request.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/_tests/test_request.py	Mon Jul 31 12:24:50 2006 +0200
@@ -123,30 +123,3 @@
                              'wrong date string')
 
 
-class GetPageNameFromQueryString(unittest.TestCase):
-    """ Test urls like http://netloc/wiki?pagename """
-
-    def setUp(self):
-        self.savedQuery = self.request.query_string
-
-    def tearDown(self):
-        self.request.query_string = self.savedQuery
-
-    def testAscii(self):
-        """ request: getPageNameFromQueryString: ascii """
-        name = expected = u'page name'
-        self.runTest(name, expected)
-
-    def testNonAscii(self):
-        """ request: getPageNameFromQueryString: non ascii """
-        name = expected = u'דף עברי'
-        self.runTest(name, expected)
-
-    def runTest(self, name, expected):
-        import urllib
-        # query as made by most browsers when you type the url into the
-        # location box.
-        query = urllib.quote(name.encode('utf-8'))
-        self.request.query_string = query
-        self.assertEqual(self.request.getPageNameFromQueryString(), expected)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_wikisync.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,36 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - MoinMoin.wikisync tests
+
+@copyright: 2006 MoinMoin:AlexanderSchremmer
+@license: GNU GPL, see COPYING for details.
+"""
+
+from unittest import TestCase
+from MoinMoin.Page import Page
+from MoinMoin.PageEditor import PageEditor
+from MoinMoin._tests import TestConfig, TestSkipped
+
+from MoinMoin.wikisync import TagStore
+
+
+class UnsafeSyncTestcase(TestCase):
+    """ Tests various things related to syncing. Note that it is not possible
+        to create pages without cluttering page revision currently, so we have to use
+        the testwiki. """
+
+    def setUp(self):
+        if not getattr(self.request.cfg, 'is_test_wiki', False):
+            raise TestSkipped('This test needs to be run using the test wiki.')
+        self.page = PageEditor(self.request, "FrontPage")
+
+    def testBasicTagThings(self):
+        tags = TagStore(self.page)
+        self.assert_(not tags.get_all_tags())
+        tags.add(remote_wiki="foo", remote_rev=1, current_rev=2)
+        tags = TagStore(self.page) # reload
+        self.assert_(tags.get_all_tags()[0].remote_rev == 1)
+    
+    def tearDown(self):
+        tags = TagStore(self.page)
+        tags.clear()
--- a/MoinMoin/action/AttachFile.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/AttachFile.py	Mon Jul 31 12:24:50 2006 +0200
@@ -493,7 +493,7 @@
     elif request.form['do'][0] == 'savedrawing':
         if request.user.may.write(pagename):
             save_drawing(pagename, request)
-            request.http_headers()
+            request.emit_http_headers()
             request.write("OK")
         else:
             msg = _('You are not allowed to save a drawing on this page.')
@@ -541,7 +541,7 @@
 def upload_form(pagename, request, msg=''):
     _ = request.getText
 
-    request.http_headers()
+    request.emit_http_headers()
     # Use user interface language for this generated page
     request.setContentLanguage(request.lang)
     request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename, msg=msg)
@@ -653,8 +653,7 @@
 
     mt = wikiutil.MimeType(filename=filename)
 
-    # send header
-    request.http_headers([
+    request.emit_http_headers([
         "Content-Type: %s" % mt.content_type(),
         "Content-Length: %d" % os.path.getsize(fpath),
         # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
@@ -824,7 +823,7 @@
     if not filename: return
 
     # send header & title
-    request.http_headers()
+    request.emit_http_headers()
     # Use user interface language for this generated page
     request.setContentLanguage(request.lang)
     title = _('attachment:%(filename)s of %(pagename)s', formatted=True) % {
--- a/MoinMoin/action/Despam.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/Despam.py	Mon Jul 31 12:24:50 2006 +0200
@@ -169,7 +169,7 @@
        # request.form.get('timestamp', [None])[0]
     ok = request.form.get('ok', [0])[0]
 
-    request.http_headers()
+    request.emit_http_headers()
     request.theme.send_title("Despam", pagename=pagename)
     # Start content (important for RTL support)
     request.write(request.formatter.startContent("content"))
--- a/MoinMoin/action/LikePages.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/LikePages.py	Mon Jul 31 12:24:50 2006 +0200
@@ -41,7 +41,7 @@
         return
 
     # more than one match, list 'em
-    request.http_headers()
+    request.emit_http_headers()
 
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
--- a/MoinMoin/action/LocalSiteMap.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/LocalSiteMap.py	Mon Jul 31 12:24:50 2006 +0200
@@ -28,7 +28,7 @@
 
 def execute(pagename, request):
     _ = request.getText
-    request.http_headers()
+    request.emit_http_headers()
 
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
--- a/MoinMoin/action/MyPages.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/MyPages.py	Mon Jul 31 12:24:50 2006 +0200
@@ -58,7 +58,7 @@
 
     from MoinMoin.Page import Page
     from MoinMoin.parser.text_moin_wiki import Parser as WikiParser
-    request.http_headers()
+    request.emit_http_headers()
 
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
--- a/MoinMoin/action/SubscribeUser.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/SubscribeUser.py	Mon Jul 31 12:24:50 2006 +0200
@@ -17,7 +17,7 @@
 
 def show_form(pagename, request):
     _ = request.getText
-    request.http_headers()
+    request.emit_http_headers()
     request.theme.send_title(_("Subscribe users to the page %s") % pagename, pagename=pagename)
 
     request.write("""
@@ -32,7 +32,7 @@
 
 def show_result(pagename, request):
     _ = request.getText
-    request.http_headers()
+    request.emit_http_headers()
 
     request.theme.send_title(_("Subscribed for %s:") % pagename, pagename=pagename)
 
--- a/MoinMoin/action/SyncPages.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/SyncPages.py	Mon Jul 31 12:24:50 2006 +0200
@@ -9,45 +9,140 @@
 """
 
 import os
+import re
 import zipfile
 import xmlrpclib
 from datetime import datetime
 
+# Compatiblity to Python 2.3
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+
 from MoinMoin import wikiutil, config, user
+from MoinMoin.packages import unpackLine, packLine
 from MoinMoin.PageEditor import PageEditor
 from MoinMoin.Page import Page
-from MoinMoin.wikidicts import Dict
+from MoinMoin.wikidicts import Dict, Group
+
 
 class ActionStatus(Exception): pass
 
+class UnsupportedWikiException(Exception): pass
+
+# Move these classes to MoinMoin.wikisync
+class RemotePage(object):
+    """ This class represents a page in (another) wiki. """
+    def __init__(self, name, revno):
+        self.name = name
+        self.revno = revno
+
+    def __repr__(self):
+        return repr("<Remote Page %r>" % unicode(self))
+
+    def __unicode__(self):
+        return u"%s<%i>" % (self.name, self.revno)
+
+    def __lt__(self, other):
+        return self.name < other.name
+
+    def __eq__(self, other):
+        if not isinstance(other, RemotePage):
+            return false
+        return self.name == other.name
+
+    def filter(cls, rp_list, regex):
+        return [x for x in rp_list if regex.match(x.name)]
+    filter = classmethod(filter)
+
+
 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):
+    def getInterwikiName(self):
+        """ Returns the interwiki name of the other wiki. """
+        return NotImplemented
+
+    def getPages(self):
+        """ Returns a list of RemotePage instances. """
+        return NotImplemented
+
+
+class MoinRemoteWiki(RemoteWiki):
+    """ Used for MoinMoin wikis reachable via XMLRPC. """
+    def __init__(self, request, interwikiname):
+        self.request = request
+        _ = self.request.getText
         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"
+        if not self.valid:
+            self.connection = None
+            return
         self.connection = self.createConnection()
+        version = self.connection.getMoinVersion()
+        if not isinstance(version, (tuple, list)):
+            raise UnsupportedWikiException(_("The remote version of MoinMoin is too old, the version 1.6 is required at least."))
+        remote_interwikiname = self.getInterwikiName()
+        remote_iwid = self.connection.interwikiName()[1]
+        self.is_anonymous = remote_interwikiname is None
+        if not self.is_anonymous and interwikiname != remote_interwikiname:
+            raise UnsupportedWikiException(_("The remote wiki uses a different InterWiki name (%(remotename)s)"
+                                             " internally than you specified (%(localname)s).") % {
+                "remotename": remote_interwikiname, "localname": interwikiname})
+
+        if self.is_anonymous:
+            self.iwid_full = packLine([remote_iwid])
+        else:
+            self.iwid_full = packLine([remote_iwid, interwikiname])
 
     def createConnection(self):
-        return xmlrpclib.ServerProxy(self.xmlrpc_url, allow_none=True)
+        return xmlrpclib.ServerProxy(self.xmlrpc_url, allow_none=True, verbose=True)
 
     # Methods implementing the RemoteWiki interface
     def getInterwikiName(self):
-        return self.connection.interwikiName()
+        return self.connection.interwikiName()[0]
+
+    def getPages(self):
+        pages = self.connection.getAllPagesEx({"include_revno": True, "include_deleted": True})
+        return [RemotePage(unicode(name), revno) for name, revno in pages]
 
     def __repr__(self):
-        return "<RemoteWiki wiki_url=%r valid=%r>" % (self.valid, self.wiki_url)
+        return "<MoinRemoteWiki wiki_url=%r valid=%r>" % (self.wiki_url, self.valid)
+
+
+class MoinLocalWiki(RemoteWiki):
+    """ Used for the current MoinMoin wiki. """
+    def __init__(self, request):
+        self.request = request
+
+    def getGroupItems(self, group_list):
+        pages = []
+        for group_pagename in group_list:
+            pages.extend(Group(self.request, group_pagename).members())
+        return [self.createRemotePage(x) for x in pages]
+
+    def createRemotePage(self, page_name):
+        return RemotePage(page_name, Page(self.request, page_name).get_real_rev())
+
+    # Methods implementing the RemoteWiki interface
+    def getInterwikiName(self):
+        return self.request.cfg.interwikiname
+
+    def getPages(self):
+        return [self.createRemotePage(x) for x in self.request.rootpage.getPageList(exists=0)]
+
+    def __repr__(self):
+        return "<MoinLocalWiki>"
+
 
 class ActionClass:
     def __init__(self, pagename, request):
@@ -55,44 +150,107 @@
         self.pagename = pagename
         self.page = Page(request, pagename)
 
-    def parsePage(self):
-        defaults = {
+    def parse_page(self):
+        options = {
             "remotePrefix": "",
             "localPrefix": "",
-            "remoteWiki": ""
+            "remoteWiki": "",
+            "localMatch": None,
+            "remoteMatch": None,
+            "pageList": None,
+            "groupList": None,
         }
+
+        options.update(Dict(self.request, self.pagename).get_dict())
+
+        # Convert page and group list strings to lists
+        if options["pageList"] is not None:
+            options["pageList"] = unpackLine(options["pageList"], ",")
+        if options["groupList"] is not None:
+            options["groupList"] = unpackLine(options["groupList"], ",")
+
+        return options
+
+    def fix_params(self, params):
+        """ Does some fixup on the parameters. """
+
+        # merge the pageList case into the remoteMatch case
+        if params["pageList"] is not None:
+            params["localMatch"] = params["remoteMatch"] = u'|'.join([r'^%s$' % re.escape(name)
+                                                                      for name in params["pageList"]])
+
+        if params["localMatch"] is not None:
+            params["localMatch"] = re.compile(params["localMatch"], re.U)
         
-        defaults.update(Dict(self.request, self.pagename).get_dict())
-        return defaults
-        
+        if params["remoteMatch"] is not None:
+            params["remoteMatch"] = re.compile(params["remoteMatch"], re.U)
+
+        return params
+
     def render(self):
         """ Render action
 
-        This action returns a wiki page with optional message, or
-        redirects to new page.
+        This action returns a status message.
         """
         _ = self.request.getText
-        
-        params = self.parsePage()
-        
+
+        params = self.fix_params(self.parse_page())
+
+
         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"])
-            
+
+            local = MoinLocalWiki(self.request)
+            try:
+                remote = MoinRemoteWiki(self.request, params["remoteWiki"])
+            except UnsupportedWikiException, (msg, ):
+                raise ActionStatus(msg)
+
             if not remote.valid:
                 raise ActionStatus(_("The ''remoteWiki'' is unknown."))
-            
-            # ...
-            self.sync(params)
+
+            self.sync(params, local, remote)
         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 sync(self, params, local, remote):
+        """ This method does the syncronisation work. """
+        
+        r_pages = remote.getPages()
+        l_pages = local.getPages()
+        print "Got %i local, %i remote pages" % (len(l_pages), len(r_pages))
+        if params["localMatch"]:
+            l_pages = RemotePage.filter(l_pages, params["localMatch"])
+        if params["remoteMatch"]:
+            print "Filtering remote pages using regex %r" % params["remoteMatch"].pattern
+            r_pages = RemotePage.filter(r_pages, params["remoteMatch"])
+        print "After filtering: Got %i local, %i remote pages" % (len(l_pages), len(r_pages))
+
+        if params["groupList"]:
+            pages_from_groupList = local.getGroupItems(params["groupList"])
+            if not params["localMatch"]:
+                l_pages = pages_from_groupList
+            else:
+                l_pages += pages_from_groupList
+
+        l_pages = set(l_pages)
+        r_pages = set(r_pages)
+        
+        # XXX this is not correct if matching is active
+        remote_but_not_local = r_pages - l_pages
+        local_but_not_remote = l_pages - r_pages
+        
+        # some initial test code
+        r_new_pages = u", ".join([unicode(x) for x in remote_but_not_local])
+        l_new_pages = u", ".join([unicode(x) for x in local_but_not_remote])
+        raise ActionStatus("These pages are in the remote wiki, but not local: " + r_new_pages + "<br>These pages are in the local wiki, but not in the remote one: " + l_new_pages)
+
+
 def execute(pagename, request):
     ActionClass(pagename, request).render()
--- a/MoinMoin/action/__init__.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/__init__.py	Mon Jul 31 12:24:50 2006 +0200
@@ -201,15 +201,8 @@
         else:
             self.render_msg(self.make_form()) # display the form again
 
-# from wikiaction.py ---------------------------------------------------------
 
-import os, re, time
-from MoinMoin import config, util
-from MoinMoin.logfile import editlog
-
-#############################################################################
-### Misc Actions
-#############################################################################
+# Builtin Actions ------------------------------------------------------------
 
 def do_raw(pagename, request):
     """ send raw content of a page (e.g. wiki markup) """
@@ -252,7 +245,7 @@
 
 def do_content(pagename, request):
     """ same as do_show, but we only show the content """
-    request.http_headers()
+    request.emit_http_headers()
     page = Page(request, pagename)
     request.write('<!-- Transclusion of %s -->' % request.getQualifiedURL(page.url(request)))
     page.send_page(request, count_hit=0, content_only=1)
@@ -279,160 +272,19 @@
     caching.CacheEntry(request, arena, "pagelinks", scope='item').remove()
     do_show(pagename, request)
 
-def do_revert(pagename, request):
-    """ restore another revision of a page as a new current revision """
-    from MoinMoin.PageEditor import PageEditor
-    _ = request.getText
-
-    if not request.user.may.revert(pagename):
-        return Page(request, pagename).send_page(request,
-            msg=_('You are not allowed to revert this page!'))
-
-    rev = int(request.form['rev'][0])
-    revstr = '%08d' % rev
-    oldpg = Page(request, pagename, rev=rev)
-    pg = PageEditor(request, pagename)
-
-    try:
-        savemsg = pg.saveText(oldpg.get_raw_body(), 0, extra=revstr,
-                              action="SAVE/REVERT")
-    except pg.SaveError, msg:
-        # msg contain a unicode string
-        savemsg = unicode(msg)
-    request.reset()
-    pg.send_page(request, msg=savemsg)
-    return None
-
 def do_goto(pagename, request):
     """ redirect to another page """
     target = request.form.get('target', [''])[0]
     request.http_redirect(Page(request, target).url(request))
 
-def do_quicklink(pagename, request):
-    """ Add the current wiki page to the user quicklinks 
-    
-    TODO: what if add or remove quicklink fail? display an error message?
-    """
-    _ = request.getText
-    msg = None
-
-    if not request.user.valid:
-        msg = _("You must login to add a quicklink.")
-    elif request.user.isQuickLinkedTo([pagename]):
-        if request.user.removeQuicklink(pagename):
-            msg = _('Your quicklink to this page has been removed.')
-    else:
-        if request.user.addQuicklink(pagename):
-            msg = _('A quicklink to this page has been added for you.')
-
-    Page(request, pagename).send_page(request, msg=msg)
-
-def do_subscribe(pagename, request):
-    """ Subscribe or unsubscribe the user to pagename
-    
-    TODO: what if subscribe failed? no message is displayed.
-    """
-    _ = request.getText
-    cfg = request.cfg
-    msg = None
-
-    if not request.user.may.read(pagename):
-        msg = _("You are not allowed to subscribe to a page you can't read.")
-
-    # Check if mail is enabled
-    elif not cfg.mail_enabled:
-        msg = _("This wiki is not enabled for mail processing.")
-
-    # Suggest visitors to login
-    elif not request.user.valid:
-        msg = _("You must log in to use subscribtions.")
-
-    # Suggest users without email to add their email address
-    elif not request.user.email:
-        msg = _("Add your email address in your UserPreferences to use subscriptions.")
-
-    elif request.user.isSubscribedTo([pagename]):
-        # Try to unsubscribe
-        if request.user.unsubscribe(pagename):
-            msg = _('Your subscribtion to this page has been removed.')
-        else:
-            msg = _("Can't remove regular expression subscription!") + u' ' + \
-                  _("Edit the subscription regular expressions in your "
-                    "UserPreferences.")
-
-    else:
-        # Try to subscribe
-        if request.user.subscribe(pagename):
-            msg = _('You have been subscribed to this page.')
-
-    Page(request, pagename).send_page(request, msg=msg)
-
 def do_userform(pagename, request):
     """ save data posted from UserPreferences """
     from MoinMoin import userform
     savemsg = userform.savedata(request)
     Page(request, pagename).send_page(request, msg=savemsg)
 
-def do_bookmark(pagename, request):
-    """ set bookmarks (in time) for RecentChanges or delete them """
-    timestamp = request.form.get('time', [None])[0]
-    if timestamp is not None:
-        if timestamp == 'del':
-            tm = None
-        else:
-            try:
-                tm = int(timestamp)
-            except StandardError:
-                tm = wikiutil.timestamp2version(time.time())
-    else:
-        tm = wikiutil.timestamp2version(time.time())
-
-    if tm is None:
-        request.user.delBookmark()
-    else:
-        request.user.setBookmark(tm)
-    Page(request, pagename).send_page(request)
-
-
-#############################################################################
-### Special Actions
-#############################################################################
-
-def do_chart(pagename, request):
-    """ Show page charts """
-    _ = request.getText
-    if not request.user.may.read(pagename):
-        msg = _("You are not allowed to view this page.")
-        return request.page.send_page(request, msg=msg)
-
-    if not request.cfg.chart_options:
-        msg = _("Charts are not available!")
-        return request.page.send_page(request, msg=msg)
-
-    chart_type = request.form.get('type', [''])[0].strip()
-    if not chart_type:
-        msg = _('You need to provide a chart type!')
-        return request.page.send_page(request, msg=msg)
-
-    try:
-        func = pysupport.importName("MoinMoin.stats." + chart_type, 'draw')
-    except (ImportError, AttributeError):
-        msg = _('Bad chart type "%s"!') % chart_type
-        return request.page.send_page(request, msg=msg)
-
-    func(pagename, request)
-
-def do_dumpform(pagename, request):
-    """ dump the form data we received in this request for debugging """
-    data = util.dumpFormData(request.form)
-
-    request.http_headers()
-    request.write("<html><body>%s</body></html>" % data)
-
-
-#############################################################################
-### Dispatching
-#############################################################################
+# Dispatching ----------------------------------------------------------------
+import os
 
 def getPlugins(request):
     """ return the path to the action plugin directory and a list of plugins there """
--- a/MoinMoin/action/backup.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/backup.py	Mon Jul 31 12:24:50 2006 +0200
@@ -28,7 +28,7 @@
     """ Send compressed tar file """
     dateStamp = time.strftime("%Y-%m-%d--%H-%M-%S-UTC", time.gmtime())
     filename = "%s-%s.tar.%s" % (request.cfg.siteid, dateStamp, request.cfg.backup_compression)
-    request.http_headers([
+    request.emit_http_headers([
         "Content-Type: application/octet-stream",
         "Content-Disposition: inline; filename=\"%s\"" % filename, ])
 
@@ -70,7 +70,7 @@
 
 def sendBackupForm(request, pagename):
     _ = request.getText
-    request.http_headers()
+    request.emit_http_headers()
     request.setContentLanguage(request.lang)
     title = _('Wiki Backup / Restore')
     request.theme.send_title(title, form=request.form, pagename=pagename)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/bookmark.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,33 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - set or delete bookmarks (in time) for RecentChanges
+
+    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>,
+                2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+import time
+
+from MoinMoin import wikiutil
+from MoinMoin.Page import Page
+
+def execute(pagename, request):
+    """ set bookmarks (in time) for RecentChanges or delete them """
+    timestamp = request.form.get('time', [None])[0]
+    if timestamp is not None:
+        if timestamp == 'del':
+            tm = None
+        else:
+            try:
+                tm = int(timestamp)
+            except StandardError:
+                tm = wikiutil.timestamp2version(time.time())
+    else:
+        tm = wikiutil.timestamp2version(time.time())
+
+    if tm is None:
+        request.user.delBookmark()
+    else:
+        request.user.setBookmark(tm)
+    request.page.send_page(request)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/chart.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,34 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - show some statistics chart
+
+    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>,
+                2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.util import pysupport
+
+def execute(pagename, request):
+    """ Show page charts """
+    _ = request.getText
+    if not request.user.may.read(pagename):
+        msg = _("You are not allowed to view this page.")
+        return request.page.send_page(request, msg=msg)
+
+    if not request.cfg.chart_options:
+        msg = _("Charts are not available!")
+        return request.page.send_page(request, msg=msg)
+
+    chart_type = request.form.get('type', [''])[0].strip()
+    if not chart_type:
+        msg = _('You need to provide a chart type!')
+        return request.page.send_page(request, msg=msg)
+
+    try:
+        func = pysupport.importName("MoinMoin.stats.%s" % chart_type, 'draw')
+    except (ImportError, AttributeError), err:
+        msg = _('Bad chart type "%s"!') % chart_type
+        return request.page.send_page(request, msg=msg)
+
+    func(pagename, request)
+
--- a/MoinMoin/action/diff.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/diff.py	Mon Jul 31 12:24:50 2006 +0200
@@ -71,7 +71,7 @@
     # This action generate content in the user language
     request.setContentLanguage(request.lang)
 
-    request.http_headers()
+    request.emit_http_headers()
     request.theme.send_title(_('Diff for "%s"') % (pagename,), pagename=pagename, allow_doubleclick=1)
 
     if rev1 > 0 and rev2 > 0 and rev1 > rev2 or rev1 == 0 and rev2 > 0:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/dumpform.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,17 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - dump form data we received (debugging)
+
+    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>,
+                2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin import util
+
+def execute(pagename, request):
+    """ dump the form data we received in this request for debugging """
+    data = util.dumpFormData(request.form)
+
+    request.emit_http_headers()
+    request.write("<html><body>%s</body></html>" % data)
+
--- a/MoinMoin/action/edit.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/edit.py	Mon Jul 31 12:24:50 2006 +0200
@@ -137,7 +137,6 @@
         try:
             still_conflict = wikiutil.containsConflictMarker(savetext)
             pg.setConflict(still_conflict)
-            request.http_headers() # XXX WHY? XXX
             savemsg = pg.saveText(savetext, rev, trivial=trivial, comment=comment)
         except pg.EditConflict, e:
             msg = e.message
--- a/MoinMoin/action/fckdialog.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/fckdialog.py	Mon Jul 31 12:24:50 2006 +0200
@@ -15,7 +15,7 @@
 
 def macro_dialog(request):
     help = get_macro_help(request)
-    request.http_headers()
+    request.emit_http_headers()
     request.write(
         '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
 <html>
@@ -169,7 +169,7 @@
         pages = [p.page_name for p in searchresult.hits]
     else:
         pages = [name]
-    request.http_headers()
+    request.emit_http_headers()
     request.write(
         '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
 <html>
@@ -200,7 +200,7 @@
 ''' % "".join(["<option>%s</option>\n" % p for p in pages]))
 
 def link_dialog(request):
-    request.http_headers()
+    request.emit_http_headers()
     # list of wiki pages
     name = request.form.get("pagename", [""])[0]
     if name:
@@ -246,7 +246,7 @@
         scriptname += "/"
     action = scriptname
     basepage = request.page.page_name.encode(config.charset)
-    request.http_headers()
+    request.emit_http_headers()
     request.write('''
 <!--
  * FCKeditor - The text editor for internet
@@ -367,7 +367,7 @@
 ##############################################################################
 
 def attachment_dialog(request):
-    request.http_headers()
+    request.emit_http_headers()
     # list of wiki pages
     name = request.form.get("pagename", [""])[0]
     if name:
@@ -397,7 +397,7 @@
     if not scriptname or scriptname[-1] != "/":
         scriptname += "/"
     action = scriptname
-    request.http_headers()
+    request.emit_http_headers()
     request.write('''
 <!--
  * FCKeditor - The text editor for internet
@@ -462,7 +462,7 @@
 ##############################################################################
 
 def image_dialog(request):
-    request.http_headers()
+    request.emit_http_headers()
     url_prefix = request.cfg.url_prefix
     request.write('''
 <!--
--- a/MoinMoin/action/fullsearch.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/fullsearch.py	Mon Jul 31 12:24:50 2006 +0200
@@ -51,8 +51,7 @@
     if len(striped) == 0:
         err = _('Please use a more selective search term instead '
                 'of {{{"%s"}}}') % needle
-        # send http headers
-        request.http_headers()
+        request.emit_http_headers()
         Page(request, pagename).send_page(request, msg=err)
         return
 
@@ -75,8 +74,7 @@
             request.http_redirect(url)
             return
 
-    # send http headers
-    request.http_headers()
+    request.emit_http_headers()
 
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
--- a/MoinMoin/action/info.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/info.py	Mon Jul 31 12:24:50 2006 +0200
@@ -8,6 +8,7 @@
                 2006 by MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
+import os
 
 from MoinMoin import config, wikiutil, action
 from MoinMoin.Page import Page
@@ -205,7 +206,7 @@
     qpagename = wikiutil.quoteWikinameURL(pagename)
     title = page.split_title(request)
 
-    request.http_headers()
+    request.emit_http_headers()
 
     # This action uses page or wiki language TODO: currently
     # page.language is broken and not available now, when we fix it,
--- a/MoinMoin/action/links.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/links.py	Mon Jul 31 12:24:50 2006 +0200
@@ -19,7 +19,7 @@
     else:
         mimetype = "text/html"
 
-    request.http_headers(["Content-Type: %s; charset=%s" % (mimetype, config.charset)])
+    request.emit_http_headers(["Content-Type: %s; charset=%s" % (mimetype, config.charset)])
 
     if mimetype == "text/html":
         request.theme.send_title(_('Full Link List for "%s"') % request.cfg.sitename)
--- a/MoinMoin/action/login.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/login.py	Mon Jul 31 12:24:50 2006 +0200
@@ -59,7 +59,7 @@
             return self.page.send_page(request, msg=error)
 
         else: # show login form
-            request.http_headers()
+            request.emit_http_headers()
             request.theme.send_title(_("Login"), pagename=self.pagename)
             # Start content (important for RTL support)
             request.write(request.formatter.startContent("content"))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/quicklink.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,29 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - add a quicklink to the user's quicklinks
+
+    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>,
+                2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.Page import Page
+
+def execute(pagename, request):
+    """ Add the current wiki page to the user quicklinks 
+    
+    TODO: what if add or remove quicklink fail? display an error message?
+    """
+    _ = request.getText
+    msg = None
+
+    if not request.user.valid:
+        msg = _("You must login to add a quicklink.")
+    elif request.user.isQuickLinkedTo([pagename]):
+        if request.user.removeQuicklink(pagename):
+            msg = _('Your quicklink to this page has been removed.')
+    else:
+        if request.user.addQuicklink(pagename):
+            msg = _('A quicklink to this page has been added for you.')
+
+    Page(request, pagename).send_page(request, msg=msg)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/revert.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,33 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - revert a page to a previous revision
+
+    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>,
+                2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.Page import Page
+
+def execute(pagename, request):
+    """ restore another revision of a page as a new current revision """
+    from MoinMoin.PageEditor import PageEditor
+    _ = request.getText
+
+    if not request.user.may.revert(pagename):
+        return Page(request, pagename).send_page(request,
+            msg=_('You are not allowed to revert this page!'))
+
+    rev = int(request.form['rev'][0])
+    revstr = '%08d' % rev
+    oldpg = Page(request, pagename, rev=rev)
+    pg = PageEditor(request, pagename)
+
+    try:
+        savemsg = pg.saveText(oldpg.get_raw_body(), 0, extra=revstr,
+                              action="SAVE/REVERT")
+    except pg.SaveError, msg:
+        # msg contain a unicode string
+        savemsg = unicode(msg)
+    request.reset()
+    pg.send_page(request, msg=savemsg)
+
--- a/MoinMoin/action/rss_rc.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/rss_rc.py	Mon Jul 31 12:24:50 2006 +0200
@@ -220,6 +220,6 @@
         httpheaders.append("Last-Modified: %s" % timefuncs.formathttpdate(lastmod))
 
     # send the generated XML document
-    request.http_headers(httpheaders)
+    request.emit_http_headers(httpheaders)
     request.write(out.getvalue())
 
--- a/MoinMoin/action/sitemap.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/sitemap.py	Mon Jul 31 12:24:50 2006 +0200
@@ -64,7 +64,7 @@
     request.user.datetime_fmt = datetime_fmt
     base = request.getBaseURL()
 
-    request.http_headers(["Content-Type: text/xml; charset=UTF-8"])
+    request.emit_http_headers(["Content-Type: text/xml; charset=UTF-8"])
 
     # we emit a piece of data so other side doesn't get bored:
     request.write("""<?xml version="1.0" encoding="UTF-8"?>\r\n""")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/subscribe.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,50 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - subscribe to a page to get notified when it changes
+
+    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>,
+                2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.Page import Page
+
+def execute(pagename, request):
+    """ Subscribe or unsubscribe the user to pagename
+    
+    TODO: what if subscribe failed? no message is displayed.
+    """
+    _ = request.getText
+    cfg = request.cfg
+    msg = None
+
+    if not request.user.may.read(pagename):
+        msg = _("You are not allowed to subscribe to a page you can't read.")
+
+    # Check if mail is enabled
+    elif not cfg.mail_enabled:
+        msg = _("This wiki is not enabled for mail processing.")
+
+    # Suggest visitors to login
+    elif not request.user.valid:
+        msg = _("You must log in to use subscribtions.")
+
+    # Suggest users without email to add their email address
+    elif not request.user.email:
+        msg = _("Add your email address in your UserPreferences to use subscriptions.")
+
+    elif request.user.isSubscribedTo([pagename]):
+        # Try to unsubscribe
+        if request.user.unsubscribe(pagename):
+            msg = _('Your subscribtion to this page has been removed.')
+        else:
+            msg = _("Can't remove regular expression subscription!") + u' ' + \
+                  _("Edit the subscription regular expressions in your "
+                    "UserPreferences.")
+
+    else:
+        # Try to subscribe
+        if request.user.subscribe(pagename):
+            msg = _('You have been subscribed to this page.')
+
+    Page(request, pagename).send_page(request, msg=msg)
+
--- a/MoinMoin/action/test.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/test.py	Mon Jul 31 12:24:50 2006 +0200
@@ -105,7 +105,7 @@
     def do_action(self):
         """ run tests """
         request = self.request
-        request.http_headers(["Content-type: text/plain; charset=%s" % config.charset])
+        request.emit_http_headers(["Content-type: text/plain; charset=%s" % config.charset])
         request.write('MoinMoin Diagnosis\n======================\n\n')
         runTest(request)
         return True, ""
--- a/MoinMoin/action/thread_monitor.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/thread_monitor.py	Mon Jul 31 12:24:50 2006 +0200
@@ -27,11 +27,11 @@
     else:
         dump_fname = "nowhere"
 
-    request.http_headers()
+    request.emit_http_headers()
     request.write('<html><body>A dump has been saved to %s.</body></html>' % dump_fname)
 
 def execute_wiki(pagename, request):
-    request.http_headers()
+    request.emit_http_headers()
 
     request.theme.send_title("Thread monitor")
     request.write('<pre>')
--- a/MoinMoin/action/titleindex.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/action/titleindex.py	Mon Jul 31 12:24:50 2006 +0200
@@ -22,7 +22,7 @@
     else:
         mimetype = "text/plain"
 
-    request.http_headers(["Content-Type: %s; charset=%s" % (mimetype, config.charset)])
+    request.emit_http_headers(["Content-Type: %s; charset=%s" % (mimetype, config.charset)])
 
     # Get list of user readable pages
     pages = request.rootpage.getPageList()
--- a/MoinMoin/config/multiconfig.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/config/multiconfig.py	Mon Jul 31 12:24:50 2006 +0200
@@ -7,9 +7,14 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import re, os, sys
-from MoinMoin import error
+import re
+import os
+import sys
+import time
+
+from MoinMoin import error, util, wikiutil
 import MoinMoin.auth as authmodule
+from MoinMoin.packages import packLine
 
 _url_re_cache = None
 _farmconfig_mtime = None
@@ -548,6 +553,31 @@
 
         # check if mail is possible and set flag:
         self.mail_enabled = (self.mail_smarthost is not None or self.mail_sendmail is not None) and self.mail_from
+        
+        self.meta_dict = wikiutil.MetaDict(os.path.join(data_dir, 'meta'), self.cache_dir)
+
+        # interwiki ID processing
+        self.load_IWID()
+
+    def load_IWID(self):
+        """ Loads the InterWikiID of this instance. It is used to identify the instance
+            globally.
+            The IWID is available as cfg.iwid
+            The full IWID containing the interwiki name is available as cfg.iwid_full
+        """
+
+        try:
+            iwid = self.meta_dict['IWID']
+        except KeyError:
+            iwid = util.random_string(16).encode("hex") + "-" + str(int(time.time()))
+            self.meta_dict['IWID'] = iwid
+            self.meta_dict.sync()
+
+        self.iwid = iwid
+        if self.interwikiname is not None:
+            self.iwid_full = packLine([iwid, self.interwikiname])
+        else:
+            self.iwid_full = packLine([iwid])
 
     def _config_check(self):
         """ Check namespace and warn about unknown names
--- a/MoinMoin/formatter/text_python.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/formatter/text_python.py	Mon Jul 31 12:24:50 2006 +0200
@@ -54,8 +54,9 @@
         waspcode_timestamp = int(time.time())
         source = ["""
 moincode_timestamp = int(os.path.getmtime(os.path.dirname(__file__)))
-if moincode_timestamp > %d or request.cfg.cfg_mtime > %d:
-    raise "CacheNeedsUpdate"
+cfg_mtime = getattr(request.cfg, "cfg_mtime", None)
+if moincode_timestamp > %d or cfg_mtime is None or cfg_mtime > %d:
+    raise Exception("CacheNeedsUpdate")
 """ % (waspcode_timestamp, waspcode_timestamp)]
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/multiconfig.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,28 @@
+""" This is just a dummy file to overwrite MoinMoin/multiconfig.py(c) from a
+    previous moin installation.
+
+    The file moved to MoinMoin/config/multiconfig.py and you have to fix your
+    imports as shown below.
+
+    Alternatively, you can temporarily set show_configuration_error = False,
+    so some compatibility code will get activated.
+    But this compatibility code will get removed soon, so you really should
+    update your config as soon as possible.
+"""
+show_configuration_error = True
+
+if show_configuration_error:
+    from MoinMoin.error import ConfigurationError
+    raise ConfigurationError("""\
+Please edit your wikiconfig/farmconfig and fix your DefaultConfig import:\r\n
+\r\n
+Old:   from MoinMoin.multiconfig import DefaultConfig\r\n
+New:   from MoinMoin.config.multiconfig import DefaultConfig\r\n
+\r\n
+If you can't do that, but if you can change the MoinMoin code, see the file
+MoinMoin/multiconfig.py for an alternative, but temporary workaround.
+""")
+
+else:
+    from MoinMoin.config.multiconfig import *
+
--- a/MoinMoin/parser/text_rst.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/parser/text_rst.py	Mon Jul 31 12:24:50 2006 +0200
@@ -381,8 +381,13 @@
                     node['classes'].append(prefix)
             else:
                 # Default case - make a link to a wiki page.
-                page = Page(self.request, refuri)
-                node['refuri'] = page.url(self.request)
+                pagename = refuri
+                anchor = ''
+                if refuri.find('#') != -1:
+                    pagename, anchor = refuri.split('#', 1)
+                    anchor = '#' + anchor
+                page = Page(self.request, pagename)
+                node['refuri'] = page.url(self.request) + anchor
                 if not page.exists():
                     node['classes'].append('nonexistent')
         html4css1.HTMLTranslator.visit_reference(self, node)
--- a/MoinMoin/request/CGI.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/CGI.py	Mon Jul 31 12:24:50 2006 +0200
@@ -63,33 +63,9 @@
             import errno
             if ex.errno != errno.EPIPE: raise
 
-    # Headers ----------------------------------------------------------
-
-    def http_headers(self, more_headers=[]):
-        # Send only once
-        if getattr(self, 'sent_headers', None):
-            return
-
-        self.sent_headers = 1
-        have_ct = 0
+    def _emit_http_headers(self, headers):
+        """ private method to send out preprocessed list of HTTP headers """
+        for header in headers:
+            self.write("%s\r\n" % header)
+        self.write("\r\n")
 
-        # send http headers
-        for header in more_headers + getattr(self, 'user_headers', []):
-            if header.lower().startswith("content-type:"):
-                # don't send content-type multiple times!
-                if have_ct: continue
-                have_ct = 1
-            if type(header) is unicode:
-                header = header.encode('ascii')
-            self.write("%s\r\n" % header)
-
-        if not have_ct:
-            self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
-
-        self.write('\r\n')
-
-        #from pprint import pformat
-        #sys.stderr.write(pformat(more_headers))
-        #sys.stderr.write(pformat(self.user_headers))
-
-
--- a/MoinMoin/request/CLI.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/CLI.py	Mon Jul 31 12:24:50 2006 +0200
@@ -78,7 +78,8 @@
     def setHttpHeader(self, header):
         pass
 
-    def http_headers(self, more_headers=[]):
+    def _emit_http_headers(self, headers):
+        """ private method to send out preprocessed list of HTTP headers """
         pass
 
     def http_redirect(self, url):
--- a/MoinMoin/request/FCGI.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/FCGI.py	Mon Jul 31 12:24:50 2006 +0200
@@ -56,31 +56,9 @@
         RequestBase.finish(self)
         self.fcgreq.finish()
 
-    # Headers ----------------------------------------------------------
-
-    def http_headers(self, more_headers=[]):
-        """ Send out HTTP headers. Possibly set a default content-type. """
-        if getattr(self, 'sent_headers', None):
-            return
-        self.sent_headers = 1
-        have_ct = 0
+    def _emit_http_headers(self, headers):
+        """ private method to send out preprocessed list of HTTP headers """
+        for header in headers:
+            self.write("%s\r\n" % header)
+        self.write("\r\n")
 
-        # send http headers
-        for header in more_headers + getattr(self, 'user_headers', []):
-            if type(header) is unicode:
-                header = header.encode('ascii')
-            if header.lower().startswith("content-type:"):
-                # don't send content-type multiple times!
-                if have_ct: continue
-                have_ct = 1
-            self.write("%s\r\n" % header)
-
-        if not have_ct:
-            self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
-
-        self.write('\r\n')
-
-        #from pprint import pformat
-        #sys.stderr.write(pformat(more_headers))
-        #sys.stderr.write(pformat(self.user_headers))
-
--- a/MoinMoin/request/MODPYTHON.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/MODPYTHON.py	Mon Jul 31 12:24:50 2006 +0200
@@ -138,51 +138,14 @@
         from mod_python import apache
         return apache.OK
 
-    # Headers ----------------------------------------------------------
-
-    def setHttpHeader(self, header):
-        """ Filters out content-type and status to set them directly
-            in the mod_python request. Rest is put into the headers_out
-            member of the mod_python request.
-
-            @param header: string, containing valid HTTP header.
-        """
-        if type(header) is unicode:
-            header = header.encode('ascii')
-        key, value = header.split(':', 1)
-        value = value.lstrip()
-        if key.lower() == 'content-type':
-            # save content-type for http_headers
-            if not self._have_ct:
-                # we only use the first content-type!
-                self.mpyreq.content_type = value
-                self._have_ct = 1
-        elif key.lower() == 'status':
-            # save status for finish
-            try:
-                self.mpyreq.status = int(value.split(' ', 1)[0])
-            except:
-                pass
-            else:
-                self._have_status = 1
-        else:
-            # this is a header we sent out
+    def _emit_http_headers(self, headers):
+        """ private method to send out preprocessed list of HTTP headers """
+        st_header, ct_header, other_headers = headers[0], headers[1], headers[2:]
+        status = st_header.split(':', 1)[1].lstrip()
+        self.mpyreq.status = int(status.split(' ', 1)[0])
+        self.mpyreq.content_type = ct_header.split(':', 1)[1].lstrip()
+        for header in other_headers:
             self.mpyreq.headers_out[key] = value
-
-    def http_headers(self, more_headers=[]):
-        """ Sends out headers and possibly sets default content-type
-            and status.
-
-            @param more_headers: list of strings, defaults to []
-        """
-        for header in more_headers + getattr(self, 'user_headers', []):
-            self.setHttpHeader(header)
-        # if we don't had an content-type header, set text/html
-        if self._have_ct == 0:
-            self.mpyreq.content_type = "text/html;charset=%s" % config.charset
-        # if we don't had a status header, set 200
-        if self._have_status == 0:
-            self.mpyreq.status = 200
         # this is for mod_python 2.7.X, for 3.X it's a NOP
         self.mpyreq.send_http_header()
 
--- a/MoinMoin/request/STANDALONE.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/STANDALONE.py	Mon Jul 31 12:24:50 2006 +0200
@@ -90,44 +90,16 @@
 
     # Headers ----------------------------------------------------------
 
-    def http_headers(self, more_headers=[]):
-        if getattr(self, 'sent_headers', None):
-            return
-
-        self.sent_headers = 1
-        user_headers = getattr(self, 'user_headers', [])
-
-        # check for status header and send it
-        our_status = 200
-        for header in more_headers + user_headers:
-            if header.lower().startswith("status:"):
-                try:
-                    our_status = int(header.split(':', 1)[1].strip().split(" ", 1)[0])
-                except:
-                    pass
-                # there should be only one!
-                break
-        # send response
-        self.sareq.send_response(our_status)
+    def _emit_http_headers(self, headers):
+        """ private method to send out preprocessed list of HTTP headers """
+        st_header, other_headers = headers[0], headers[1:]
+        status = st_header.split(':', 1)[1].lstrip()
+        status_code, status_msg = status.split(' ', 1)
+        status_code = int(status_code)
+        self.sareq.send_response(status_code, status_msg)
+        for header in other_headers:
+            key, value = header.split(':', 1)
+            value = value.lstrip()
+            self.sareq.send_header(key, value)
+        self.sareq.end_headers()
 
-        # send http headers
-        have_ct = 0
-        for header in more_headers + user_headers:
-            if type(header) is unicode:
-                header = header.encode('ascii')
-            if header.lower().startswith("content-type:"):
-                # don't send content-type multiple times!
-                if have_ct: continue
-                have_ct = 1
-
-            self.write("%s\r\n" % header)
-
-        if not have_ct:
-            self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
-
-        self.write('\r\n')
-
-        #from pprint import pformat
-        #sys.stderr.write(pformat(more_headers))
-        #sys.stderr.write(pformat(self.user_headers))
-
--- a/MoinMoin/request/TWISTED.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/TWISTED.py	Mon Jul 31 12:24:50 2006 +0200
@@ -46,7 +46,7 @@
             RequestBase.__init__(self, properties)
 
         except MoinMoinFinish: # might be triggered by http_redirect
-            self.http_headers() # send headers (important for sending MOIN_ID cookie)
+            self.emit_http_headers() # send headers (important for sending MOIN_ID cookie)
             self.finish()
 
         except Exception, err:
@@ -110,34 +110,20 @@
 
     # Headers ----------------------------------------------------------
 
-    def __setHttpHeader(self, header):
-        if type(header) is unicode:
-            header = header.encode('ascii')
-        key, value = header.split(':', 1)
-        value = value.lstrip()
-        if key.lower() == 'set-cookie':
-            key, value = value.split('=', 1)
-            self.twistd.addCookie(key, value)
-        else:
-            self.twistd.setHeader(key, value)
-        #print "request.RequestTwisted.setHttpHeader: %s" % header
-
-    def http_headers(self, more_headers=[]):
-        if getattr(self, 'sent_headers', None):
-            return
-        self.sent_headers = 1
-        have_ct = 0
-
-        # set http headers
-        for header in more_headers + getattr(self, 'user_headers', []):
-            if header.lower().startswith("content-type:"):
-                # don't send content-type multiple times!
-                if have_ct: continue
-                have_ct = 1
-            self.__setHttpHeader(header)
-
-        if not have_ct:
-            self.__setHttpHeader("Content-type: text/html;charset=%s" % config.charset)
+    def _emit_http_headers(self, headers):
+        """ private method to send out preprocessed list of HTTP headers """
+        st_header, other_headers = headers[0], headers[1:]
+        status = st_header.split(':', 1)[1].lstrip()
+        status_code, status_msg = status.split(' ', 1)
+        self.twistd.setResponseCode(status_code, status_message)
+        for header in other_headers:
+            key, value = header.split(':', 1)
+            value = value.lstrip()
+            if key.lower() == 'set-cookie':
+                key, value = value.split('=', 1)
+                self.twistd.addCookie(key, value)
+            else:
+                self.twistd.setHeader(key, value)
 
     def http_redirect(self, url):
         """ Redirect to a fully qualified, or server-rooted URL 
@@ -151,6 +137,3 @@
         #self.twistd.finish()
         raise MoinMoinFinish
 
-    def setResponseCode(self, code, message=None):
-        self.twistd.setResponseCode(code, message)
-
--- a/MoinMoin/request/WSGI.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/WSGI.py	Mon Jul 31 12:24:50 2006 +0200
@@ -21,6 +21,7 @@
             self.stdin = env['wsgi.input']
             self.stdout = StringIO.StringIO()
 
+            # used by MoinMoin.server.wsgi:
             self.status = '200 OK'
             self.headers = []
 
@@ -48,33 +49,14 @@
     def reset_output(self):
         self.stdout = StringIO.StringIO()
 
-    def setHttpHeader(self, header):
-        if type(header) is unicode:
-            header = header.encode('ascii')
-
-        key, value = header.split(':', 1)
-        value = value.lstrip()
-        if key.lower() == 'content-type':
-            # save content-type for http_headers
-            if self.hasContentType:
-                # we only use the first content-type!
-                return
-            else:
-                self.hasContentType = True
-
-        elif key.lower() == 'status':
-            # save status for finish
-            self.status = value
-            return
-
-        self.headers.append((key, value))
-
-    def http_headers(self, more_headers=[]):
-        for header in more_headers:
-            self.setHttpHeader(header)
-
-        if not self.hasContentType:
-            self.headers.insert(0, ('Content-Type', 'text/html;charset=%s' % config.charset))
+    def _emit_http_headers(self, headers):
+        """ private method to send out preprocessed list of HTTP headers """
+        st_header, other_headers = headers[0], headers[1:]
+        self.status = st_header.split(':', 1)[1].lstrip()
+        for header in other_headers:
+            key, value = header.split(':', 1)
+            value = value.lstrip()
+            self.headers.append((key, value))
 
     def flush(self):
         pass
@@ -83,6 +65,7 @@
         pass
 
     def output(self):
+        # called by MoinMoin.server.wsgi
         return self.stdout.getvalue()
 
 
--- a/MoinMoin/request/__init__.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/request/__init__.py	Mon Jul 31 12:24:50 2006 +0200
@@ -9,7 +9,7 @@
 
 import os, re, time, sys, cgi, StringIO
 import copy
-from MoinMoin import config, wikiutil, user, caching
+from MoinMoin import config, wikiutil, user, caching, error
 from MoinMoin.util import IsWin9x
 
 
@@ -17,7 +17,11 @@
 
 class MoinMoinFinish(Exception):
     """ Raised to jump directly to end of run() function, where finish is called """
-    pass
+
+
+class HeadersAlreadySentException(Exception):
+    """ Is raised if the headers were already sent when emit_http_headers is called."""
+
 
 # Timing ---------------------------------------------------------------
 
@@ -93,7 +97,6 @@
         # Pages meta data that we collect in one request
         self.pages = {}
 
-        self.sent_headers = 0
         self.user_headers = []
         self.cacheable = 0 # may this output get cached by http proxies/caches?
         self.page = None
@@ -618,18 +621,6 @@
             return ''
         return self.script_name
 
-    def getPageNameFromQueryString(self):
-        """ Try to get pagename from the query string
-        
-        Support urls like http://netloc/script/?page_name. Allow
-        solving path_info encoding problems by calling with the page
-        name as a query.
-        """
-        pagename = wikiutil.url_unquote(self.query_string, want_unicode=False)
-        pagename = self.decodePagename(pagename)
-        pagename = self.normalizePagename(pagename)
-        return pagename
-
     def getKnownActions(self):
         """ Create a dict of avaiable actions
 
@@ -981,13 +972,12 @@
         }
         headers = [
             'Status: %d %s' % (resultcode, statusmsg[resultcode]),
-            'Content-Type: text/plain'
+            'Content-Type: text/plain; charset=utf-8'
         ]
         # when surge protection triggered, tell bots to come back later...
         if resultcode == 503:
             headers.append('Retry-After: %d' % self.cfg.surge_lockout_time)
-        self.http_headers(headers)
-        self.setResponseCode(resultcode)
+        self.emit_http_headers(headers)
         self.write(msg)
         self.forbidden = True
 
@@ -1081,8 +1071,6 @@
 
             # 3. Or handle action
             else:
-                if not pagename and self.query_string:
-                    pagename = self.getPageNameFromQueryString()
                 # pagename could be empty after normalization e.g. '///' -> ''
                 # Use localized FrontPage if pagename is empty
                 if not pagename:
@@ -1129,7 +1117,85 @@
         @param url: relative or absolute url, ascii using url encoding.
         """
         url = self.getQualifiedURL(url)
-        self.http_headers(["Status: 302 Found", "Location: %s" % url])
+        self.emit_http_headers(["Status: 302 Found", "Location: %s" % url])
+
+    def http_headers(self, more_headers=[]):
+        """ wrapper for old, deprecated http_headers call,
+            new code only calls emit_http_headers.
+            Remove in moin 1.7.
+        """
+        self.emit_http_headers(more_headers)
+
+    def emit_http_headers(self, more_headers=[]):
+        """ emit http headers after some preprocessing / checking
+
+            Makes sure we only emit headers once.
+            Encodes to ASCII if it gets unicode headers.
+            Make sure we have exactly one Content-Type and one Status header.
+            Make sure Status header string begins with a integer number.
+        
+            For emitting, it calls the server specific _emit_http_headers
+            method.
+
+            @param more_headers: list of additional header strings
+        """
+        user_headers = getattr(self, 'user_headers', [])
+        self.user_headers = []
+        all_headers = more_headers + user_headers
+
+        # Send headers only once
+        sent_headers = getattr(self, 'sent_headers', 0)
+        self.sent_headers = sent_headers + 1
+        if sent_headers:
+            raise HeadersAlreadySentException("emit_http_headers called multiple (%d) times! Headers: %r" % (sent_headers, headers))
+        #else:
+        #    self.log("Notice: emit_http_headers called first time. Headers: %r" % all_headers)
+
+        content_type = None
+        status = None
+        headers = []
+        # assemble complete list of http headers
+        for header in all_headers:
+            if isinstance(header, unicode):
+                header = header.encode('ascii')
+            key, value = header.split(':', 1)
+            lkey = key.lower()
+            value = value.lstrip()
+            if content_type is None and lkey == "content-type":
+                content_type = value
+            elif status is None and lkey == "status":
+                status = value
+            else:
+                headers.append(header)
+
+        if content_type is None:
+            content_type = "text/html; charset=%s" % config.charset
+        ct_header = "Content-type: %s" % content_type
+
+        if status is None:
+            status = "200 OK"
+        try:
+            int(status.split(" ", 1)[0])
+        except:
+            self.log("emit_http_headers called with invalid header Status: %s" % status)
+            status = "500 Server Error - invalid status header"
+        st_header = "Status: %s" % status
+
+        headers = [st_header, ct_header] + headers # do NOT change order!
+        self._emit_http_headers(headers)
+
+        #from pprint import pformat
+        #sys.stderr.write(pformat(headers))
+
+    def _emit_http_headers(self, headers):
+        """ server specific method to emit http headers.
+        
+            @param headers: a list of http header strings in this FIXED order:
+                1. status header (always present and valid, e.g. "200 OK")
+                2. content type header (always present)
+                3. other headers (optional)
+        """
+        raise NotImplementedError
 
     def setHttpHeader(self, header):
         """ Save header for later send.
@@ -1140,6 +1206,9 @@
         self.user_headers.append(header)
 
     def setResponseCode(self, code, message=None):
+        """ DEPRECATED, will vanish in moin 1.7,
+            just use a Status: <code> <message> header and emit_http_headers.
+        """
         pass
 
     def fail(self, err):
@@ -1153,8 +1222,9 @@
         @param err: Exception instance or subclass.
         """
         self.failed = 1 # save state for self.run()            
-        self.http_headers(['Status: 500 MoinMoin Internal Error'])
-        self.setResponseCode(500)
+        # we should not generate the headers two times
+        if not getattr(self, 'sent_headers', 0):
+            self.emit_http_headers(['Status: 500 MoinMoin Internal Error'])
         self.log('%s: %s' % (err.__class__.__name__, str(err)))
         from MoinMoin import failure
         failure.handle(self)
--- a/MoinMoin/script/migration/data.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/script/migration/data.py	Mon Jul 31 12:24:50 2006 +0200
@@ -39,7 +39,7 @@
         meta_fname = os.path.join(data_dir, 'meta')
         while True:
             try:
-                meta = wikiutil.MetaDict(meta_fname)
+                meta = wikiutil.MetaDict(meta_fname, request.cfg.cache_dir)
                 try:
                     curr_rev = meta['data_format_revision']
                     mig_name = str(curr_rev)
--- a/MoinMoin/stats/hitcounts.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/stats/hitcounts.py	Mon Jul 31 12:24:50 2006 +0200
@@ -243,12 +243,11 @@
         (request.cfg.chart_options['width'], request.cfg.chart_options['height']),
         image, days)
 
-    # send HTTP headers
     headers = [
         "Content-Type: image/gif",
         "Content-Length: %d" % len(image.getvalue()),
     ]
-    request.http_headers(headers)
+    request.emit_http_headers(headers)
 
     # copy the image
     image.reset()
--- a/MoinMoin/stats/pagesize.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/stats/pagesize.py	Mon Jul 31 12:24:50 2006 +0200
@@ -114,12 +114,11 @@
         (request.cfg.chart_options['width'], request.cfg.chart_options['height']),
         image, labels)
 
-    # send HTTP headers
     headers = [
         "Content-Type: image/gif",
         "Content-Length: %d" % len(image.getvalue()),
     ]
-    request.http_headers(headers)
+    request.emit_http_headers(headers)
 
     # copy the image
     image.reset()
--- a/MoinMoin/stats/useragents.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/stats/useragents.py	Mon Jul 31 12:24:50 2006 +0200
@@ -172,12 +172,11 @@
         (request.cfg.chart_options['width'], request.cfg.chart_options['height']),
         image, labels)
 
-    # send HTTP headers
     headers = [
         "Content-Type: image/gif",
         "Content-Length: %d" % len(image.getvalue()),
     ]
-    request.http_headers(headers)
+    request.emit_http_headers(headers)
 
     # copy the image
     image.reset()
--- a/MoinMoin/support/cgitb.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/support/cgitb.py	Mon Jul 31 12:24:50 2006 +0200
@@ -70,6 +70,11 @@
 __UNDEF__ = [] # a special sentinel object
 
 
+class HiddenObject:
+    def __repr__(self):
+        return "<HIDDEN>"
+HiddenObject = HiddenObject()
+
 class HTMLFormatter:
     """ Minimal html formatter """
     
@@ -295,7 +300,10 @@
             if ttype == tokenize.NAME and token not in keyword.kwlist:
                 if lasttoken == '.':
                     if parent is not __UNDEF__:
-                        value = getattr(parent, token, __UNDEF__)
+                        if self.unsafe_name(token):
+                            value = HiddenObject
+                        else:
+                            value = getattr(parent, token, __UNDEF__)
                         vars.append((prefix + token, prefix, value))
                 else:
                     where, value = self.lookup(token)
@@ -324,8 +332,12 @@
                 value = builtins.get(name, __UNDEF__)
             else:
                 value = getattr(builtins, name, __UNDEF__)
+        if self.unsafe_name(name):
+            value = HiddenObject
         return scope, value
 
+    def unsafe_name(self, name):
+        return name in self.frame.f_globals.get("unsafe_names", ())
 
 class View:
     """ Traceback view """
--- a/MoinMoin/support/thfcgi.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/support/thfcgi.py	Mon Jul 31 12:24:50 2006 +0200
@@ -327,17 +327,18 @@
         self.have_finished = 1
 
         # stderr
-        self.err.reset()
-        rec = Record()
-        rec.rec_type = FCGI_STDERR
-        rec.req_id = self.req_id
-        data = self.err.read()
-        while data:
-            chunk, data = self.getNextChunk(data)
-            rec.content = chunk
-            rec.writeRecord(self.conn)
-        rec.content = ""
-        rec.writeRecord(self.conn)      # Terminate stream
+        if self.err.tell(): # just send err record if there is data on the err stream
+            self.err.reset()
+            rec = Record()
+            rec.rec_type = FCGI_STDERR
+            rec.req_id = self.req_id
+            data = self.err.read()
+            while data:
+                chunk, data = self.getNextChunk(data)
+                rec.content = chunk
+                rec.writeRecord(self.conn)
+            rec.content = ""
+            rec.writeRecord(self.conn)      # Terminate stream
 
         # stdout
         self.out.reset()
--- a/MoinMoin/user.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/user.py	Mon Jul 31 12:24:50 2006 +0200
@@ -6,6 +6,9 @@
     @license: GNU GPL, see COPYING for details.
 """
 
+# add names here to hide them in the cgitb traceback
+unsafe_names = ("id", "key", "val", "user_data", "enc_password")
+
 import os, time, sha, codecs
 
 try:
@@ -289,9 +292,9 @@
             self.language = 'en'
 
     def __repr__(self):
-        return "<%s.%s at 0x%x name:%r id:%s valid:%r>" % (
+        return "<%s.%s at 0x%x name:%r valid:%r>" % (
             self.__class__.__module__, self.__class__.__name__,
-            id(self), self.name, self.id, self.valid)
+            id(self), self.name, self.valid)
 
     def make_id(self):
         """ make a new unique user id """
--- a/MoinMoin/util/__init__.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/util/__init__.py	Mon Jul 31 12:24:50 2006 +0200
@@ -7,7 +7,7 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import os, sys, re
+import os, sys, re, random
 
 #############################################################################
 ### XML helper functions
@@ -112,3 +112,7 @@
     def close(self):
         self.buffer = None
 
+
+def random_string(length):
+    chars = ''.join([chr(random.randint(0, 255)) for x in xrange(length)])
+    return chars
--- a/MoinMoin/util/lock.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/util/lock.py	Mon Jul 31 12:24:50 2006 +0200
@@ -11,9 +11,9 @@
 
 # Temporary debugging aid, to be replaced with system wide debuging
 # in release 3000.
-import sys
-def log(msg):
-    sys.stderr.write('[%s] lock: %s' % (time.asctime(), msg))
+#import sys
+#def log(msg):
+#    sys.stderr.write('[%s] lock: %s' % (time.asctime(), msg))
 
 
 class Timer:
--- a/MoinMoin/wikidicts.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/wikidicts.py	Mon Jul 31 12:24:50 2006 +0200
@@ -26,7 +26,7 @@
 
 # Version of the internal data structure which is pickled
 # Please increment if you have changed the structure
-DICTS_PICKLE_VERSION = 4
+DICTS_PICKLE_VERSION = 5
 
 
 class DictBase:
@@ -44,13 +44,18 @@
         """
         self.name = name
 
-        self.regex = re.compile(self.regex, re.MULTILINE | re.UNICODE)
+        self.initRegex()
 
         # Get text from page named 'name'
         p = Page.Page(request, name)
         text = p.get_raw_body()
         self.initFromText(text)
 
+    def initRegex(cls):
+        """ Make it a class attribute to avoid it being pickled. """
+        cls.regex = re.compile(cls.regex, re.MULTILINE | re.UNICODE)
+    initRegex = classmethod(initRegex)
+
     def initFromText(self, text):
         raise NotImplementedError('sub classes should override this')
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/wikisync.py	Mon Jul 31 12:24:50 2006 +0200
@@ -0,0 +1,116 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Wiki Synchronisation
+
+    @copyright: 2006 by MoinMoin:AlexanderSchremmer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import os
+
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+from MoinMoin.util import lock
+
+
+class Tag(object):
+    """ This class is used to store information about merging state. """
+    
+    def __init__(self, remote_wiki, remote_rev, current_rev):
+        """ Creates a new Tag.
+        
+        @param remote_wiki: The identifier of the remote wiki.
+        @param remote_rev: The revision number on the remote end.
+        @param current_rev: The related local revision.
+        """
+        self.remote_wiki = remote_wiki
+        self.remote_rev = remote_rev
+        self.current_rev = current_rev
+
+    def __repr__(self):
+        return u"<Tag remote_wiki=%r remote_rev=%r current_rev=%r>" % (self.remote_wiki, self.remote_rev, self.current_rev)
+
+
+class AbstractTagStore(object):
+    """ This class is an abstract base class that shows how to implement classes
+        that manage the storage of tags. """
+
+    def __init__(self, page):
+        """ Subclasses don't need to call this method. It is just here to enforce
+        them having accept a page argument at least. """
+        pass
+
+    def add(self, **kwargs):
+        """ Adds a Tag object to the current TagStore. """
+        print "Got tag for page %r: %r" % (self.page, kwargs)
+        return NotImplemented
+
+    def get_all_tags(self):
+        """ Returns a list of all Tag objects associated to this page. """
+        return NotImplemented
+    
+    def clear(self):
+        """ Removes all tags. """
+        return NotImplemented
+
+
+class PickleTagStore(AbstractTagStore):
+    """ This class manages the storage of tags in pickle files. """
+
+    def __init__(self, page):
+        """ Creates a new TagStore that uses pickle files.
+        
+        @param page: a Page object where the tags should be related to
+        """
+        
+        self.page = page
+        self.filename = page.getPagePath('synctags', use_underlay=0, check_create=1, isfile=1)
+        lock_dir = os.path.join(page.getPagePath('cache', use_underlay=0, check_create=1), '__taglock__')
+        self.rlock = lock.ReadLock(lock_dir, 60.0)
+        self.wlock = lock.WriteLock(lock_dir, 60.0)
+        self.load()
+
+    def load(self):
+        """ Loads the tags from the data file. """
+        if not self.rlock.acquire(3.0):
+            raise EnvironmentError("Could not lock in PickleTagStore")
+        try:
+            try:
+                datafile = file(self.filename, "rb")
+            except IOError:
+                self.tags = []
+            else:
+                self.tags = pickle.load(datafile)
+                datafile.close()
+        finally:
+            self.rlock.release()
+    
+    def commit(self):
+        """ Writes the memory contents to the data file. """
+        if not self.wlock.acquire(3.0):
+            raise EnvironmentError("Could not lock in PickleTagStore")
+        try:
+            datafile = file(self.filename, "wb")
+            pickle.dump(self.tags, datafile, protocol=pickle.HIGHEST_PROTOCOL)
+            datafile.close()
+        finally:
+            self.wlock.release()
+
+    # public methods ---------------------------------------------------
+    def add(self, **kwargs):
+        self.tags.append(Tag(**kwargs))
+        self.commit()
+    
+    def get_all_tags(self):
+        return self.tags
+
+    def clear(self):
+        self.tags = []
+        self.commit()
+
+# currently we just have one implementation, so we do not need
+# a factory method
+TagStore = PickleTagStore
\ No newline at end of file
--- a/MoinMoin/wikiutil.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/wikiutil.py	Mon Jul 31 12:24:50 2006 +0200
@@ -6,11 +6,16 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import os, re, urllib, cgi
-import codecs, types
+import cgi
+import codecs
+import os
+import re
+import time
+import types
+import urllib
 
 from MoinMoin import util, version, config
-from MoinMoin.util import pysupport, filesys
+from MoinMoin.util import pysupport, filesys, lock
 
 # Exceptions
 class InvalidFileNameError(Exception):
@@ -403,13 +408,18 @@
                 ]
 
 class MetaDict(dict):
-    """ store meta informations as a dict """
-    def __init__(self, metafilename):
+    """ store meta informations as a dict.
+    XXX It is not thread-safe, add locks!
+    """
+    def __init__(self, metafilename, cache_directory):
         """ create a MetaDict from metafilename """
         dict.__init__(self)
         self.metafilename = metafilename
         self.dirty = False
         self.loaded = False
+        lock_dir = os.path.join(cache_directory, '__metalock__')
+        self.rlock = lock.ReadLock(lock_dir, 60.0)
+        self.wlock = lock.WriteLock(lock_dir, 60.0)
 
     def _get_meta(self):
         """ get the meta dict from an arbitrary filename.
@@ -417,11 +427,16 @@
             @param metafilename: the name of the file to read
             @return: dict with all values or {} if empty or error
         """
-        # XXX what does happen if the metafile is being written to in another process?
+
         try:
-            metafile = codecs.open(self.metafilename, "r", "utf-8")
-            meta = metafile.read() # this is much faster than the file's line-by-line iterator
-            metafile.close()
+            if not self.rlock.acquire(3.0):
+                raise EnvironmentError("Could not lock in MetaDict")
+            try:
+                metafile = codecs.open(self.metafilename, "r", "utf-8")
+                meta = metafile.read() # this is much faster than the file's line-by-line iterator
+                metafile.close()
+            finally:
+                self.rlock.release()
         except IOError:
             meta = u''
         for line in meta.splitlines():
@@ -443,16 +458,21 @@
             if key in INTEGER_METAS:
                 value = str(value)
             meta.append("%s: %s" % (key, value))
-        meta = '\n'.join(meta)
-        # XXX what does happen if the metafile is being read or written to in another process?
-        metafile = codecs.open(self.metafilename, "w", "utf-8")
-        metafile.write(meta)
-        metafile.close()
+        meta = '\r\n'.join(meta)
+
+        if not self.wlock.acquire(5.0):
+            raise EnvironmentError("Could not lock in MetaDict")
+        try:
+            metafile = codecs.open(self.metafilename, "w", "utf-8")
+            metafile.write(meta)
+            metafile.close()
+        finally:
+            self.wlock.release()
         filesys.chmod(self.metafilename, 0666 & config.umask)
         self.dirty = False
 
     def sync(self, mtime_usecs=None):
-        """ sync the in-memory dict to disk (if dirty) """
+        """ sync the in-memory dict to the persistent store (if dirty) """
         if self.dirty:
             if not mtime_usecs is None:
                 self.__setitem__('mtime', str(mtime_usecs))
@@ -469,6 +489,8 @@
                 raise
 
     def __setitem__(self, key, value):
+        """ Sets a dictionary entry. You actually have to call sync to write it
+            to the persistent store. """
         try:
             oldvalue = dict.__getitem__(self, key)
         except KeyError:
@@ -483,9 +505,16 @@
 #############################################################################
 def load_wikimap(request):
     """ load interwiki map (once, and only on demand) """
+
+    now = int(time.time())
+
     try:
         _interwiki_list = request.cfg._interwiki_list
+        if request.cfg._interwiki_ts + (3*60) < now: # 3 minutes caching time
+            raise AttributeError # refresh cache
     except AttributeError:
+        from MoinMoin.Page import Page
+
         _interwiki_list = {}
         lines = []
 
@@ -493,7 +522,7 @@
         # precedence over the shared one, and is thus read AFTER
         # the shared one
         intermap_files = request.cfg.shared_intermap
-        if not isinstance(intermap_files, type([])):
+        if not isinstance(intermap_files, list):
             intermap_files = [intermap_files]
         intermap_files.append(os.path.join(request.cfg.data_dir, "intermap.txt"))
 
@@ -503,6 +532,9 @@
                 lines.extend(f.readlines())
                 f.close()
 
+        # add the contents of the InterWikiMap page
+        lines += Page(request, "InterWikiMap").get_raw_body().splitlines()
+
         for line in lines:
             if not line or line[0] == '#': continue
             try:
@@ -522,6 +554,7 @@
 
         # save for later
         request.cfg._interwiki_list = _interwiki_list
+        request.cfg._interwiki_ts = now
 
     return _interwiki_list
 
--- a/MoinMoin/xmlrpc/__init__.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/MoinMoin/xmlrpc/__init__.py	Mon Jul 31 12:24:50 2006 +0200
@@ -20,7 +20,8 @@
     when really necessary (like for transferring binary files like
     attachments maybe).
 
-    @copyright: 2003-2005 by Thomas Waldmann
+    @copyright: 2003-2006 MoinMoin:ThomasWaldmann
+    @copyright: 2004-2006 MoinMoin:AlexanderSchremmer
     @license: GNU GPL, see COPYING for details
 """
 from MoinMoin.util import pysupport
@@ -58,7 +59,7 @@
         @rtype: str
         @return: string in config.charset
         """
-        raise "NotImplementedError"
+        raise NotImplementedError("please implement _instr in derived class")
 
     def _outstr(self, text):
         """ Convert outbound string to utf-8.
@@ -67,7 +68,7 @@
         @rtype: str
         @return: string in utf-8
         """
-        raise "NotImplementedError"
+        raise NotImplementedError("please implement _outstr in derived class")
 
     def _inlob(self, text):
         """ Convert inbound base64-encoded utf-8 to Large OBject.
@@ -130,8 +131,8 @@
             # serialize it
             response = xmlrpclib.dumps(response, methodresponse=1)
 
-        self.request.http_headers([
-            "Content-Type: text/xml;charset=utf-8",
+        self.request.emit_http_headers([
+            "Content-Type: text/xml; charset=utf-8",
             "Content-Length: %d" % len(response),
         ])
         self.request.write(response)
@@ -219,10 +220,39 @@
         """ Get all pages readable by current user
 
         @rtype: list
-        @return: a list of all pages. The result is a list of utf-8 strings.
+        @return: a list of all pages.
         """
-        pagelist = self.request.rootpage.getPageList()
-        return map(self._outstr, pagelist)
+
+        # the official WikiRPC interface is implemented by the extended method
+        # as well
+        return self.xmlrpc_getAllPagesEx()
+
+
+    def xmlrpc_getAllPagesEx(self, opts=None):
+        """ Get all pages readable by current user. Not an WikiRPC method.
+
+        @param opts: dictionary that can contain the following arguments:
+                include_system:: set it to false if you do not want to see system pages
+                include_revno:: set it to True if you want to have lists with [pagename, revno]
+                include_deleted:: set it to True if you want to include deleted pages
+        @rtype: list
+        @return: a list of all pages.
+        """
+        options = {"include_system": True, "include_revno": False, "include_deleted": False}
+        if opts is not None:
+            options.update(opts)
+
+        if not options["include_system"]:
+            filter = lambda name: not wikiutil.isSystemPage(self.request, name)
+        else:
+            filter = lambda name: True
+
+        pagelist = self.request.rootpage.getPageList(filter=filter, exists=not options["include_deleted"])
+        
+        if options['include_revno']:
+            return [[self._outstr(x), Page(self.request, x).get_real_rev()] for x in pagelist]
+        else:
+            return [self._outstr(x) for x in pagelist]
 
     def xmlrpc_getRecentChanges(self, date):
         """ Get RecentChanges since date
@@ -498,6 +528,7 @@
         from MoinMoin import version
         return (version.project, version.release, version.revision)
 
+
     # authorization methods
 
     def xmlrpc_getAuthToken(self, username, password, *args):
@@ -519,6 +550,9 @@
         else:
             return xmlrpclib.Fault("INVALID", "Invalid token.")
 
+
+    # methods for wiki synchronization
+
     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
@@ -555,13 +589,13 @@
         if from_rev is None:
             oldcontents = lambda: ""
         else:
-            oldpage = Page(request, pagename, rev=from_rev)
+            oldpage = Page(self.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)
+            newpage = Page(self.request, pagename, rev=to_rev)
             newcontents = lambda: newpage.get_raw_body_str()
             newrev = newpage.get_real_rev()
 
@@ -575,12 +609,13 @@
         return {"conflict": conflict, "diff": diffblob, "diffversion": 1, "current": currentpage.get_real_rev()}
 
     def xmlrpc_interwikiName(self):
-        """ Returns the interwiki name of the current wiki. """
+        """ Returns the interwiki name and the IWID of the current wiki. """
         name = self.request.cfg.interwikiname
+        iwid = self.request.cfg.iwid
         if name is None:
-            return None
+            return [None, iwid]
         else:
-            return self._outstr(name)
+            return [self._outstr(name), iwid]
 
     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.
@@ -594,9 +629,13 @@
             @param interwiki_name: Used to build the interwiki tag.
         """
         from MoinMoin.util.bdiff import decompress, patch
+        from MoinMoin.wikisync import TagStore
+        LASTREV_INVALID = xmlrpclib.Fault("LASTREV_INVALID", "The page was changed")
 
         pagename = self._instr(pagename)
 
+        comment = u"Remote - %r" % interwiki_name
+        
         # User may read page?
         if not self.request.user.may.read(pagename) or not self.request.user.may.write(pagename):
             return self.notAllowedFault()
@@ -604,10 +643,10 @@
         # XXX add locking here!
 
         # current version of the page
-        currentpage = Page(self.request, pagename)
+        currentpage = PageEditor(self.request, pagename, do_editor_backup=0)
 
         if currentpage.get_real_rev() != last_remote_rev:
-            return xmlrpclib.Fault("LASTREV_INVALID", "The page was changed")
+            return LASTREV_INVALID
 
         if not currentpage.exists() and diff is None:
             return xmlrpclib.Fault("NOT_EXIST", "The page does not exist and no diff was supplied.")
@@ -619,11 +658,19 @@
         newcontents = patch(basepage.get_raw_body_str(), decompress(str(diff)))
 
         # write page
-        # XXX ...
+        try:
+            currentpage.saveText(newcontents.encode("utf-8"), last_remote_rev, comment=comment)
+        except PageEditor.EditConflict:
+            return LASTREV_INVALID
 
-        # XXX add a tag (interwiki_name, local_rev, current rev) to the page
-        # XXX return current rev
-        # XXX finished
+        current_rev = currentpage.get_real_rev()
+        
+        tags = TagStore(currentpage)
+        tags.add(remote_wiki=interwiki_name, remote_rev=local_rev, current_rev=current_rev)
+
+        # XXX unlock page
+
+        return current_rev
 
 
     # XXX BEGIN WARNING XXX
--- a/docs/CHANGES	Sat Jul 29 23:19:55 2006 +0200
+++ b/docs/CHANGES	Mon Jul 31 12:24:50 2006 +0200
@@ -81,11 +81,11 @@
       will be missing and the adaptor script will need a change maybe):
       CGI works
       CLI works
-      STANDALONE not
-      MODPY not
-      WSGI not
-      FCGI not
-      TWISTED not
+      STANDALONE ?
+      MODPY ?
+      WSGI ?
+      FCGI ?
+      TWISTED ?
     * moved util/antispam.py to security/antispam.py,
       moved util/autoadmin.py to security/autoadmin.py,
       moved security.py to security/__init__.py,
@@ -119,6 +119,24 @@
       TODO: write mig script for data_dir
       TODO: make blanks in interwiki pagelinks possible
     * request.action now has the action requested, default: 'show'.
+    * Cleaned up duplicated http_headers code and DEPRECATED this function
+      call (it was sometimes confused with setHttpHeaders call) - it will
+      vanish with moin 1.7, so please fix your custom plugins!
+      The replacement is:
+          request.emit_http_headers(more_headers=[])
+      This call pre-processes the headers list (encoding from unicode, making
+      sure that there is exactly ONE content-type header, etc.) and then
+      calls a server specific helper _emit_http_headers to emit it.
+      CGI works
+      CLI ?
+      STANDALONE ?
+      MODPY ?
+      WSGI ?
+      FCGI ?
+      TWISTED ?
+    * setResponseCode request method DEPRECATED (it only worked for Twisted
+      anyway), just use emit_http_headers and include a Status: XXX header.
+      Method will vanish with moin 1.7. 
 
   New Features:
     * Removed "underscore in URL" == "blank in pagename magic" - it made more
@@ -165,7 +183,7 @@
     * The i18n system no loads *.po files directly (no *.py or *.mo any more)
       and caches the results (farm wide cache/i18n/*).
     * added the diff parser from ParserMarket, thanks to Emilio Lopes, Fabien
-      Ninoles and Jürgen Hermann.
+      Ninoles and Jrgen Hermann.
 
   Bugfixes:
     * on action "info" page, "revert" link will not be displayed for empty page
@@ -186,6 +204,7 @@
     * Added a (less broken) MoinMoin.support.difflib, details see there.
     * BadContent and LocalBadContent now get noindex,nofollow robots header,
       same as POSTs.
+    * Fixed handling of anchors in wiki links for the Restructured text parser.
 
   Other changes:
     * we use (again) the same browser compatibility check as FCKeditor uses
@@ -194,7 +213,8 @@
       at FCKeditor development or browser development.
     * HINT: instead of "from MoinMoin.multiconfig import DefaultConfig" you
       need to use "from MoinMoin.config.multiconfig import DefaultConfig" now.
-      You need to change this in you wikiconfig.py or farmconfig.py file.
+      You need to change this in your wikiconfig.py or farmconfig.py file.
+      See MoinMoin/multiconfig.py for an alternative way if you can't do that.
 
 Version 1.5.4-current:
     * increased maxlength of some input fields from 80 to 200
@@ -859,7 +879,7 @@
 
   International support:    
     * mail_from can be now a unicode name-address 
-      e.g u'Jürgen wiki <noreply@jhwiki.org>'
+      e.g u'Jrgen wiki <noreply@jhwiki.org>'
 
   Theme changes:
     * logo_string is now should be really only the logo (img).
@@ -2352,8 +2372,7 @@
         || {{{ ;)) }}} || ;)) || lol.gif    ||
     * AbandonedPages macro
     * Added definition list markup: {{{<whitespace>term:: definition}}}
-    * Added email notification features contributed by Daniel Saß
-    * SystemInfo: show "Entries in edit log"
+    * Added email notification features contributed by Daniel Sa�    * SystemInfo: show "Entries in edit log"
     * Added "RSS" icon to RecentChanges macro and code to generate a
       RecentChanges RSS channel, see
           http://www.usemod.com/cgi-bin/mb.pl?UnifiedRecentChanges
@@ -2626,7 +2645,7 @@
       there before a new version is written to disk
     * Removed the "Reset" button from EditPage
     * Added "Reduce editor size" link
-    * Added Latin-1 WikiNames (JürgenHermann ;)
+    * Added Latin-1 WikiNames (JrgenHermann ;)
     * Speeded up RecentChanges by looking up hostnames ONCE while saving
     * Show at most 14 (distinct) days in RecentChanges
     * Added icons for common functions, at the top of the page
@@ -2639,7 +2658,7 @@
     * Grey background for code sections
     * Added handling for numbered lists
     * the edit textarea now grows in width with the browser window
-      (thanks to Sebastian Dauß for that idea)
+      (thanks to Sebastian Dau�for that idea)
     * Added page info (revision history) and viewing of old revisions
     * Added page diff, and diff links on page info
     * Added InterWiki support (use "wiki:WikiServer/theirlocalname"; the list
--- a/docs/CHANGES.aschremmer	Sat Jul 29 23:19:55 2006 +0200
+++ b/docs/CHANGES.aschremmer	Mon Jul 31 12:24:50 2006 +0200
@@ -2,16 +2,17 @@
 ===============================
 
   Known main issues:
-    * ...
+    * How will we store tags? (Metadata support would be handy)
+    * How to handle renames/deletes?
+    * How to handle colliding/empty interwiki names?
 
   ToDo:
     * Implement actual syncronisation.
     * 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?
+    * Add page locking, i.e. use the one in the new storage layer.
+    * Check what needs to be documented on MoinMaster.
 
   New Features:
     * XMLRPC method to return the Moin version
@@ -20,10 +21,25 @@
     * XMLRPC Authentication System
     * Binary Diffing
     * XMLRPC method to get binary diffs
-    * 
+    * XMLRPC method to merge remote changes locally
+    * XMLRPC method to get the interwiki name
+    * TagStore/PickleTagStore class
+    * XMLRPC method to get the pagelist in a special way (revnos,
+      no system pages etc.)
+    * IWID support - i.e. every instance has a unique ID
+    * InterWiki page editable in the wiki
 
   Bugfixes (only stuff that is buggy in moin/1.6 main branch):
-    * Conflict resolution fixes.
+    * Conflict resolution fixes. (merged into main)
+    * Python 2.5 compatibility fixes in the Page caching logic (merged)
+    * sre pickle issues in the wikidicts code (merged)
+    * cgitb can hide particular names, this avoids information leaks
+      if the user files cannot be parsed for example
+    * Fixed User.__repr__ - it is insane to put the ID in there
+    * Worked around the FastCGI problem on Lighttpd: empty lines in the error log, thanks to Jay Soffian
+    * Fixed the MetaDict code to use locks.
+    * Fixed bug in request.py that avoided showing a traceback if there was a fault
+      after the first headers were sent.
 
   Other Changes:
     * Refactored conflict resolution and XMLRPC code.
@@ -58,6 +74,24 @@
 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.
+Week 29: Finished first version of the mergeChanges method. Added Tag and TagStore classes which are currently
+         using pickle-based storage. Added getAllPagesEx XMLRPC method.
+Week 30: Implemented IWID support, added function to generate random strings. Added support
+         for editing the InterWikiMap in the wiki. Added locking to the PickleTagStore and the MetaDict classes. Added handling of
+         various options and detection of anonymous wikis to the SyncPages action.
+
+2006-07-18: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-19: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-20: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-21: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-22: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-23: no work on SOC project -- a Sunday
+2006-07-24: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-25: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-26: student didnt work on project
+2006-07-27: student didnt work on project
+2006-07-28: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
+2006-07-29: the requested daily entry is missing here, see http://moinmoin.wikiwikiweb.de/GoogleSoc2006/BetterProgress
 
 Time plan
 =========
--- a/setup.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/setup.py	Mon Jul 31 12:24:50 2006 +0200
@@ -219,6 +219,7 @@
         'MoinMoin',
         'MoinMoin.action',
         'MoinMoin.auth',
+        'MoinMoin.config',
         'MoinMoin.converter',
         'MoinMoin.filter',
         'MoinMoin.formatter',
--- a/wikiconfig.py	Sat Jul 29 23:19:55 2006 +0200
+++ b/wikiconfig.py	Mon Jul 31 12:24:50 2006 +0200
@@ -6,7 +6,7 @@
 @license: GNU GPL, see COPYING for details.
 """
 
-from MoinMoin.multiconfig import DefaultConfig
+from MoinMoin.config.multiconfig import DefaultConfig
 
 
 class Config(DefaultConfig):