changeset 5487:91aa8c3c515b

merged moin/1.7
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Wed, 03 Feb 2010 13:37:38 +0100
parents f9a7aa1a4a4a (diff) a283079b3f1e (current diff)
children b0dfed9a569f 9241bdfaeb6d
files MoinMoin/action/newaccount.py MoinMoin/userform/admin.py
diffstat 1327 files changed, 170961 insertions(+), 137369 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Feb 03 13:35:28 2010 +0100
+++ b/.hgignore	Wed Feb 03 13:37:38 2010 +0100
@@ -8,4 +8,5 @@
 ^wikiconfig_local.*
 ^MoinMoin/i18n/POTFILES(\.in)?$
 .coverage
+.DS_Store
 
--- a/.hgtags	Wed Feb 03 13:35:28 2010 +0100
+++ b/.hgtags	Wed Feb 03 13:37:38 2010 +0100
@@ -22,6 +22,18 @@
 9901ffff5280b81d0476e8d1e434ea70e3a6fbdc 1.7.0rc2
 01ef230fb671b0f0636328b04ade9b44c5548327 1.7.0rc3
 761c3a503be2b97d8e7beb902751dbb5e60f3127 1.7.0
+b1e192a3651a57aa451fefca06f5a849e1e4b422 SOC2008-END
 da9e664b3f518f0cab89f7ebf22b99ecac0eaaa1 1.7.1
 ae9bf455eec6a080dd6aafacdaf30fb881b1ca68 1.7.2
 ffdb44bf3b7391f660735fd509015819e8feaad2 1.7.3
+2907390f9d4630f5459506d3d3352c96f75e67b3 1.8.0beta1
+117a21659358defd880baaccf5c73e7b1a71ea1e 1.8.0beta2
+df1ba1ddf05061d6451797869c159b3c60319470 1.8.0beta3
+4186e90ead069aad4b739f278eb8599dc00ff6a4 1.8.0rc1
+7fee549f435d119801075f0d18fc57822f78f0eb 1.8.0
+fad2936d33a7eac9370f3cdc28f0ec375518d5cf 1.8.1
+1f0db10c207f697de8d496f298167d7de76f30ac 1.8.2
+f25e6286fe1306017cda1bc614f5a9f60b382670 1.8.3
+3010c1a941856920ee564297f16570126b0231c0 1.8.4
+294b97b991d3b394aa7cf16ce18b01d8a64e6ef0 1.8.5
+137fcd650f26acbca8a964e0ed21aa378747b71c 1.8.6
--- a/Makefile	Wed Feb 03 13:35:28 2010 +0100
+++ b/Makefile	Wed Feb 03 13:37:38 2010 +0100
@@ -3,7 +3,7 @@
 #
 
 # location for the wikiconfig.py we use for testing:
-export PYTHONPATH=$(PWD)/tests:$(PWD)
+export PYTHONPATH=$(PWD)
 
 testwiki := ./tests/wiki
 share := ./wiki
@@ -13,9 +13,9 @@
 
 install-docs:
 	-mkdir build
-	wget -U MoinMoin/Makefile -O build/INSTALL.html "http://master17.moinmo.in/MoinMoin/InstallDocs?action=print"
+	wget -U MoinMoin/Makefile -O build/INSTALL.html "http://master18.moinmo.in/MoinMoin/InstallDocs?action=print"
 	sed \
-		-e 's#href="/#href="http://master17.moinmo.in/#g' \
+		-e 's#href="/#href="http://master18.moinmo.in/#g' \
 		-e 's#http://[a-z\.]*/wiki/classic/#/wiki/classic/#g' \
 		-e 's#http://[a-z\.]*/wiki/modern/#/wiki/modern/#g' \
 		-e 's#http://[a-z\.]*/wiki/rightsidebar/#/wiki/rightsidebar/#g' \
@@ -25,9 +25,9 @@
         build/INSTALL.html >docs/INSTALL.html
 	-rm build/INSTALL.html
 
-	wget -U MoinMoin/Makefile -O build/UPDATE.html "http://master17.moinmo.in/HelpOnUpdating?action=print"
+	wget -U MoinMoin/Makefile -O build/UPDATE.html "http://master18.moinmo.in/HelpOnUpdating?action=print"
 	sed \
-		-e 's#href="/#href="http://master17.moinmo.in/#g' \
+		-e 's#href="/#href="http://master18.moinmo.in/#g' \
 		-e 's#http://[a-z\.]*/wiki/classic/#/wiki/classic/#g' \
 		-e 's#http://[a-z\.]*/wiki/modern/#/wiki/modern/#g' \
 		-e 's#http://[a-z\.]*/wiki/rightsidebar/#/wiki/rightsidebar/#g' \
@@ -39,7 +39,7 @@
 	-rmdir build
 
 interwiki:
-	wget -U MoinMoin/Makefile -O $(share)/data/intermap.txt "http://master17.moinmo.in/InterWikiMap?action=raw"
+	wget -U MoinMoin/Makefile -O $(share)/data/intermap.txt "http://master18.moinmo.in/InterWikiMap?action=raw"
 	chmod 664 $(share)/data/intermap.txt
 
 check-tabs:
@@ -47,21 +47,21 @@
 
 # Create documentation
 epydoc: patchlevel
-	@epydoc -o ../html-1.7 --name=MoinMoin --url=http://moinmo.in/ --graph=all --graph-font=Arial MoinMoin
+	@epydoc -o ../html-1.8 --name=MoinMoin --url=http://moinmo.in/ --graph=all --graph-font=Arial MoinMoin
 
 # Create new underlay directory from MoinMaster
 # Should be used only on TW machine
 underlay:
 	rm -rf $(share)/underlay
-	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.7 --wiki-url=master17.moinmo.in/ maint globaledit
-	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.7 --wiki-url=master17.moinmo.in/ maint reducewiki --target-dir=$(share)/underlay
+	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.8 --wiki-url=master18.moinmo.in/ maint globaledit
+	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.8 --wiki-url=master18.moinmo.in/ maint reducewiki --target-dir=$(share)/underlay
 	rm -rf $(share)/underlay/pages/InterWikiMap
 	rm -rf $(share)/underlay/pages/MoinPagesEditorGroup
 	cd $(share); rm -f underlay.tar; tar cf underlay.tar underlay
 
 pagepacks:
 	@python MoinMoin/_tests/maketestwiki.py
-	@MoinMoin/script/moin.py --config-dir=$(testwiki)/.. maint mkpagepacks
+	@MoinMoin/script/moin.py --config-dir=MoinMoin/_tests maint mkpagepacks
 	cd $(share) ; rm -rf underlay
 	cp -a $(testwiki)/underlay $(share)/
 	
--- a/MoinMoin/Page.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/Page.py	Wed Feb 03 13:37:38 2010 +0100
@@ -554,7 +554,7 @@
             if editordata[0] == 'interwiki':
                 editor = "%s:%s" % editordata[1]
             else:
-                editor = editordata[1] # ip or email
+                editor = editordata[1] # ip or email or anon
             result = {
                 'timestamp': line.ed_time_usecs,
                 'editor': editor,
@@ -755,7 +755,10 @@
 
         # Add anchor
         if anchor:
-            url = "%s#%s" % (url, wikiutil.url_quote_plus(anchor))
+            fmt = getattr(self, 'formatter', request.html_formatter)
+            if fmt:
+                anchor = fmt.sanitize_to_id(anchor)
+            url = "%s#%s" % (url, anchor)
 
         if not relative:
             url = '%s/%s' % (request.getScriptname(), url)
@@ -1026,10 +1029,11 @@
             # redirect to another page
             # note that by including "action=show", we prevent endless looping
             # (see code in "request") or any cascaded redirection
-            request.http_redirect('%s/%s?action=show&redirect=%s' % (
-                request.getScriptname(),
-                wikiutil.quoteWikinameURL(pi['redirect']),
-                wikiutil.url_quote_plus(self.page_name, ''), ))
+            pagename, anchor = wikiutil.split_anchor(pi['redirect'])
+            redirect_url = Page(request, pagename).url(request,
+                                                       querystr={'action': 'show', 'redirect': self.page_name, },
+                                                       anchor=anchor)
+            request.http_redirect(redirect_url, code=301)
             return
 
         # if necessary, load the formatter
--- a/MoinMoin/PageEditor.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/PageEditor.py	Wed Feb 03 13:37:38 2010 +0100
@@ -24,6 +24,7 @@
 from MoinMoin.widget import html
 from MoinMoin.widget.dialog import Status
 from MoinMoin.logfile import editlog, eventlog
+from MoinMoin.mail.sendmail import encodeSpamSafeEmail
 from MoinMoin.support.python_compatibility import set
 from MoinMoin.util import filesys, timefuncs, web
 from MoinMoin.events import PageDeletedEvent, PageRenamedEvent, PageCopiedEvent, PageRevertedEvent
@@ -483,7 +484,8 @@
 
         # QuickHelp originally by Georg Mischler <schorsch@lightingwiki.com>
         markup = self.pi['format'] or request.cfg.default_markup
-        quickhelp = request.cfg.editor_quickhelp.get(markup, "")
+        parser = wikiutil.searchAndImportPlugin(self.request.cfg, "parser", markup)
+        quickhelp = getattr(parser, 'quickhelp', None)
         if quickhelp:
             request.write(request.formatter.div(1, id="editor-help"))
             request.write(_(quickhelp, wiki=True))
@@ -763,6 +765,7 @@
         request = self.request
         now = self._get_local_timestamp()
         u = request.user
+        obfuscated_email_address = encodeSpamSafeEmail(u.email)
         signature = u.signature()
         variables = {
             'PAGE': self.page_name,
@@ -772,6 +775,7 @@
             'USERNAME': signature,
             'USER': "-- %s" % signature,
             'SIG': "-- %s <<DateTime(%s)>>" % (signature, now),
+            'EMAIL': "<<MailTo(%s)>>" % (obfuscated_email_address)
         }
 
         if u.valid and u.name:
--- a/MoinMoin/PageGraphicalEditor.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/PageGraphicalEditor.py	Wed Feb 03 13:37:38 2010 +0100
@@ -314,7 +314,7 @@
         url_prefix_static = request.cfg.url_prefix_static
         url_prefix_local = request.cfg.url_prefix_local
         wikipage = wikiutil.quoteWikinameURL(self.page_name)
-        fckbasepath = url_prefix_local + '/applets/FCKeditor'
+        fckbasepath = request.cfg.url_prefix_fckeditor
         wikiurl = request.getScriptname()
         if not wikiurl or wikiurl[-1] != '/':
             wikiurl += '/'
--- a/MoinMoin/__init__.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/__init__.py	Wed Feb 03 13:37:38 2010 +0100
@@ -1,9 +1,9 @@
 # -*- coding: iso-8859-1 -*-
 """
-MoinMoin Version 1.7.3
+MoinMoin - a wiki engine in Python
 
 @copyright: 2000-2006 by Juergen Hermann <jh@web.de>,
-            2002-2008 MoinMoin:ThomasWaldmann
+            2002-2009 MoinMoin:ThomasWaldmann
 @license: GNU GPL, see COPYING for details.
 """
 
--- a/MoinMoin/_tests/_test_template.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/_tests/_test_template.py	Wed Feb 03 13:37:38 2010 +0100
@@ -45,19 +45,17 @@
         ('Line break',  '<<BR>>',        '<br>'),
     )
 
+    from MoinMoin._tests import wikiconfig
+    class Config(wikiconfig.Config):
+        foo = 'bar'  # we want to have this non-default setting
+
     def setup_class(self):
         """ Stuff that should be run to init the state of this test class
-
-        Some test needs specific config values, or they will fail.
         """
-        self.config = self.TestConfig(defaults=['this option', 'that option'],
-                                      another_option='non default value')
 
     def teardown_class(self):
         """ Stuff that should run to clean up the state of this test class
-
         """
-        self.config.reset()
 
     def testFunction(self):
         """ module_tested: function should... """
--- a/MoinMoin/_tests/compat.py	Wed Feb 03 13:35:28 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-""" UnitTest compatiblity code, from the py lib. MIT licensed originally, copyright Holger Krekel. """
-
-import py
-from py.__.test.outcome import Failed, Passed
-
-
-class TestCaseUnit(py.test.collect.Function):
-    """ compatibility Unit executor for TestCase methods
-        honouring setUp and tearDown semantics.
-    """
-    def execute(self, session):
-        boundmethod = self.obj
-        instance = boundmethod.im_self
-        instance.setUp()
-        try:
-            boundmethod()
-        finally:
-            instance.tearDown()
-        return Passed()
-
-class TestCase(object):
-    """compatibility class of unittest's TestCase. """
-    Function = TestCaseUnit
-
-    def setUp(self):
-        pass
-
-    def tearDown(self):
-        pass
-
-    def fail(self, msg=None):
-        """ fail immediate with given message. """
-        raise Failed(msg=msg)
-
-    def assertRaises(self, excclass, func, *args, **kwargs):
-        py.test.raises(excclass, func, *args, **kwargs)
-    failUnlessRaises = assertRaises
-
-    # dynamically construct (redundant) methods
-    aliasmap = [
-        ('x',   'not x', 'assert_, failUnless'),
-        ('x',   'x',     'failIf'),
-        ('x,y', 'x!=y',  'failUnlessEqual,assertEqual, assertEquals'),
-        ('x,y', 'x==y',  'failIfEqual,assertNotEqual, assertNotEquals'),
-        ]
-    items = []
-    for sig, expr, names in aliasmap:
-        names = map(str.strip, names.split(','))
-        sigsubst = expr.replace('y', '%s').replace('x', '%s')
-        for name in names:
-            items.append("""
-                def %(name)s(self, %(sig)s, msg=""):
-                    __tracebackhide__ = True
-                    if %(expr)s:
-                        raise Failed(msg=msg + (%(sigsubst)r %% (%(sig)s)))
-            """ % locals() )
-
-    source = "".join(items)
-    exec py.code.Source(source).compile()
-
-__all__ = ['TestCase']
--- a/MoinMoin/_tests/test_PageEditor.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/_tests/test_PageEditor.py	Wed Feb 03 13:37:38 2010 +0100
@@ -73,11 +73,7 @@
 
     def testExtendedNamesEnabled(self):
         """ PageEditor: expand @USERNAME@ extended name - enabled """
-        try:
-            config = self.TestConfig()
-            assert self.expand() == u'[[%s]]' % self.name
-        finally:
-            del config
+        assert self.expand() == u'[[%s]]' % self.name
 
 
 class TestExpandMailto(TestExpandUserName):
@@ -210,6 +206,7 @@
         """
         simple test if it is possible to delete a Dict page after creation
         """
+        become_trusted(self.request)
         pagename = u'SomeDict'
         page = PageEditor(self.request, pagename, do_editor_backup=0)
         body = u"This is an example text"
--- a/MoinMoin/_tests/test_sourcecode.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/_tests/test_sourcecode.py	Wed Feb 03 13:37:38 2010 +0100
@@ -32,6 +32,8 @@
 
 try:
     import xattr
+    if not hasattr(xattr, "xattr"): # there seem to be multiple modules with that name
+        raise ImportError
     def mark_file_ok(path, mtime):
         x = xattr.xattr(path)
         try:
--- a/MoinMoin/_tests/test_user.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/_tests/test_user.py	Wed Feb 03 13:37:38 2010 +0100
@@ -3,6 +3,7 @@
     MoinMoin - MoinMoin.user Tests
 
     @copyright: 2003-2004 by Juergen Hermann <jh@web.de>
+                2009 by ReimarBauer
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -19,17 +20,17 @@
     def testAscii(self):
         """user: encode ascii password"""
         # u'MoinMoin' and 'MoinMoin' should be encoded to same result
-        expected = "{SHA}X+lk6KR7JuJEH43YnmettCwICdU="
+        expected = "{SSHA}xkDIIx1I7A4gC98Vt/+UelIkTDYxMjM0NQ=="
 
-        result = user.encodePassword("MoinMoin")
+        result = user.encodePassword("MoinMoin", salt='12345')
         assert result == expected
-        result = user.encodePassword(u"MoinMoin")
+        result = user.encodePassword(u"MoinMoin", salt='12345')
         assert result == expected
 
     def testUnicode(self):
         """ user: encode unicode password """
-        result = user.encodePassword(u'סיסמה סודית בהחלט') # Hebrew
-        expected = "{SHA}GvvkgYzv5MoF9Ljivv2oc81FmkE="
+        result = user.encodePassword(u'סיסמה סודית בהחלט', salt='12345') # Hebrew
+        expected = "{SSHA}YiwfeVWdVW9luqyVn8t2JivlzmUxMjM0NQ=="
         assert result == expected
 
 
@@ -99,49 +100,12 @@
         theUser = user.User(self.request, name=name, password=password)
         assert theUser.valid
 
-    def testOldNonAsciiPassword(self):
-        """ user: login with non-ascii password in pre 1.3 user file
-
-        When trying to login with an old non-ascii password in the user
-        file, utf-8 encoded password will not match. In this case, try
-        all other encoding available on pre 1.3 before failing.
-        """
-        # Create test user
-        # Use iso charset to create user with old enc_password, as if
-        # the user file was migrated from pre 1.3 wiki.
-        name = u'__Jürgen Herman__'
-        password = name
-        self.createUser(name, password, charset='iso-8859-1')
-
-        # Try to "login"
-        theUser = user.User(self.request, name=name, password=password)
-        assert theUser.valid
-
-    def testReplaceOldNonAsciiPassword(self):
-        """ user: login replace old non-ascii password in pre 1.3 user file
-
-        When trying to login with an old non-ascii password in the user
-        file, the password hash should be replaced with new utf-8 hash.
-        """
-        # Create test user
-        # Use iso charset to create user with old enc_password, as if
-        # the user file was migrated from pre 1.3 wiki.
-        name = u'__Jürgen Herman__'
-        password = name
-        self.createUser(name, password, charset='iso-8859-1')
-        # Login - this should replace the old password in the user file
-        theUser = user.User(self.request, name=name, password=password)
-        # Login again - the password should be new unicode password
-        expected = user.encodePassword(password)
-        theUser = user.User(self.request, name=name, password=password)
-        assert theUser.enc_password == expected
-
     def testSubscriptionSubscribedPage(self):
         """ user: tests isSubscribedTo  """
         pagename = u'HelpMiscellaneous'
         name = u'__Jürgen Herman__'
         password = name
-        self.createUser(name, password, charset='iso-8859-1')
+        self.createUser(name, password)
         # Login - this should replace the old password in the user file
         theUser = user.User(self.request, name=name, password=password)
         theUser.subscribe(pagename)
@@ -153,7 +117,7 @@
         testPagename = u'HelpMiscellaneous/FrequentlyAskedQuestions'
         name = u'__Jürgen Herman__'
         password = name
-        self.createUser(name, password, charset='iso-8859-1')
+        self.createUser(name, password)
         # Login - this should replace the old password in the user file
         theUser = user.User(self.request, name=name, password=password)
         theUser.subscribe(pagename)
@@ -164,8 +128,6 @@
         if the old username is removed from the cache name2id
         """
         # Create test user
-        # Use iso charset to create user with old enc_password, as if
-        # the user file was migrated from pre 1.3 wiki.
         name = u'__Some Name__'
         password = name
         self.createUser(name, password)
@@ -178,20 +140,52 @@
 
         assert not theUser.exists()
 
+    def test_upgrade_password_to_salted(self):
+        """
+        Create user with {SHA} password and check that logging in
+        upgrades to {SSHA}.
+        """
+        name = u'/no such user/'
+        password = '{SHA}jLIjfQZ5yojbZGTqxg2pY0VROWQ=' # 12345
+        self.createUser(name, password, True)
+        theuser = user.User(self.request, name=name, password='12345')
+        assert theuser.enc_password[:6] == '{SSHA}'
+
+    def test_for_email_attribute_by_name(self):
+        """
+        checks for no access to the email attribute by getting the user object from name
+        """
+        name = u"__TestUser__"
+        password = u"ekfdweurwerh"
+        email = "__TestUser__@moinhost"
+        self.createUser(name, password, email=email)
+        theuser = user.User(self.request, name=name)
+        assert theuser.email == ""
+
+    def test_for_email_attribut_by_uid(self):
+        """
+        checks access to the email attribute by getting the user object from the uid
+        """
+        name = u"__TestUser2__"
+        password = u"ekERErwerwerh"
+        email = "__TestUser2__@moinhost"
+        self.createUser(name, password, email=email)
+        uid = user.getUserId(self.request, name)
+        theuser = user.User(self.request, uid)
+        assert theuser.email == email
+
     # Helpers ---------------------------------------------------------
 
-    def createUser(self, name, password, charset='utf-8'):
+    def createUser(self, name, password, pwencoded=False, email=None):
         """ helper to create test user
-
-        charset is used to create user with pre 1.3 password hash
         """
-        # Hack self.request form to contain the password
-        self.request.form['password'] = [password]
-
         # Create user
         self.user = user.User(self.request)
         self.user.name = name
-        self.user.enc_password = user.encodePassword(password, charset=charset)
+        self.user.email = email
+        if not pwencoded:
+            password = user.encodePassword(password)
+        self.user.enc_password = password
 
         # Validate that we are not modifying existing user data file!
         if self.user.exists():
@@ -209,19 +203,9 @@
 
 class TestGroupName(object):
 
-    def setUp(self):
-        self.config = self.TestConfig(page_group_regex=r'.+Group')
-
-    def tearDown(self):
-        del self.config
-
-    import re
-    group = re.compile(r'.+Group', re.UNICODE)
-
     def testGroupNames(self):
         """ user: isValidName: reject group names """
         test = u'AdminGroup'
-        assert self.group.search(test)
         assert not user.isValidName(self.request, test)
 
 
--- a/MoinMoin/_tests/test_wikiutil.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/_tests/test_wikiutil.py	Wed Feb 03 13:37:38 2010 +0100
@@ -909,9 +909,13 @@
         tests = [
             # text                    expected output
             (u'\xf6\xf6ll\xdf\xdf',   'A.2BAPYA9g-ll.2BAN8A3w-'),
-            (u'level 2',              'level2'),
+            (u'level 2',              'level_2'),
+            (u'level_2',              'level_2'),
             (u'',                     'A'),
             (u'123',                  'A123'),
+            # make sure that a valid anchor is not modified:
+            (u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:_.-',
+             u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:_.-')
         ]
         for text, expected in tests:
             yield self._check, text, expected
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/wikiconfig.py	Wed Feb 03 13:37:38 2010 +0100
@@ -0,0 +1,33 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - test wiki configuration
+
+Do not change any values without good reason.
+
+We mostly want to have default values here, except for stuff that doesn't
+work without setting them (like data_dir and underlay_dir).
+
+@copyright: 2000-2004 by Juergen Hermann <jh@web.de>
+@license: GNU GPL, see COPYING for details.
+"""
+
+import os
+
+from MoinMoin.config.multiconfig import DefaultConfig
+
+
+class Config(DefaultConfig):
+    sitename = u'Developer Test Wiki'
+    logo_string = sitename
+
+    _base_dir = os.path.join(os.path.dirname(__file__), '../../tests/wiki')
+    data_dir = os.path.join(_base_dir, "data")
+    data_underlay_dir = os.path.join(_base_dir, "underlay")
+
+    #show_hosts = 1
+
+    #secrets = 'some not secret string just to make tests happy'
+
+    # used to check if it is really a wiki we may modify
+    is_test_wiki = True
+
--- a/MoinMoin/action/AttachFile.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/AttachFile.py	Wed Feb 03 13:37:38 2010 +0100
@@ -1041,7 +1041,7 @@
                 fmt.url(0))
         request.write('For using an external program follow this link %s' % link)
         return
-    request.write(m.execute('EmbedObject', u'target=%s, pagename=%s' % (filename, pagename)))
+    request.write(m.execute('EmbedObject', u'target="%s", pagename="%s"' % (filename, pagename)))
     return
 
 
@@ -1110,7 +1110,7 @@
 
         browser = DataBrowserWidget(request)
         browser.setData(data)
-        return browser.toHTML()
+        return browser.render(method="GET")
 
     return ''
 
--- a/MoinMoin/action/CopyPage.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/CopyPage.py	Wed Feb 03 13:37:38 2010 +0100
@@ -4,8 +4,8 @@
 
     This action allows you to copy a page.
 
-    @copyright: 2007 MoinMoin:ReimarBauer,
-                2007 MoinMoin:ThomasWaldmann
+    @copyright: 2007 MoinMoin:ThomasWaldmann,
+                2007-2009 MoinMoin:ReimarBauer
     @license: GNU GPL, see COPYING for details.
 """
 import re
--- a/MoinMoin/action/Despam.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/Despam.py	Wed Feb 03 13:37:38 2010 +0100
@@ -71,7 +71,7 @@
 
     table = DataBrowserWidget(request)
     table.setData(dataset)
-    table.render()
+    return table.render(method="GET")
 
 class tmp:
     pass
@@ -197,7 +197,7 @@
     elif editor:
         show_pages(request, pagename, editor, timestamp)
     else:
-        show_editors(request, pagename, timestamp)
+        request.write(show_editors(request, pagename, timestamp))
 
     # End content and send footer
     request.write(request.formatter.endContent())
--- a/MoinMoin/action/RenamePage.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/RenamePage.py	Wed Feb 03 13:37:38 2010 +0100
@@ -53,6 +53,9 @@
         self.page = PageEditor(self.request, self.pagename)
         success, msgs = self.page.renamePage(newpagename, comment)
 
+        if not success:
+            return success, msgs
+
         rename_subpages = 0
         if 'rename_subpages' in form:
             try:
@@ -73,7 +76,7 @@
     def do_action_finish(self, success):
         if success:
             url = Page(self.request, self.newpagename).url(self.request)
-            self.request.http_redirect(url)
+            self.request.http_redirect(url, code=301)
         else:
             self.render_msg(self.make_form(), "dialog")
 
--- a/MoinMoin/action/SubscribeUser.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/SubscribeUser.py	Wed Feb 03 13:37:38 2010 +0100
@@ -1,19 +1,20 @@
 """
    MoinMoin - Subscribeuser - Action
-   Subscribe a user to a page
+   Subscribe (or unsubscribe) a user to a page.
 
    @copyright: 2003 Daniela Nicklas <nicklas@informatik.uni-stuttgart.de>,
-               2005 MoinMoin:AlexanderSchremmer
+               2005 MoinMoin:AlexanderSchremmer,
+               2009 MoinMoin:ThomasWaldmann
    @license: GNU GPL, see COPYING for details.
 """
 
-import sys, os
-#sys.path.append("YOUR CONFIG DIRECTORY HERE")
+import sys, os, re
 
 from MoinMoin.Page import Page
 from MoinMoin import user
 from MoinMoin import wikiutil
 
+
 def show_form(pagename, request):
     _ = request.getText
     request.emit_http_headers()
@@ -30,6 +31,31 @@
     request.theme.send_footer(pagename)
     request.theme.send_closing_html()
 
+
+def parse_re(usernames):
+    username_regexes = []
+    for name in usernames:
+        if name.startswith("re:"):
+            name = name[3:]
+        else:
+            name = re.escape(name)
+        username_regexes.append(name)
+    return username_regexes
+
+
+def parse_userlist(usernames):
+    subscribe = []
+    unsubscribe = []
+    for name in usernames:
+        if name.startswith("-"):
+            unsubscribe.append(name[1:])
+        elif name.startswith("+"):
+            subscribe.append(name[1:])
+        else:
+            subscribe.append(name)
+    return parse_re(subscribe), parse_re(unsubscribe)
+
+
 def show_result(pagename, request):
     _ = request.getText
     request.emit_http_headers()
@@ -39,50 +65,65 @@
     from MoinMoin.formatter.text_html import Formatter
     formatter = Formatter(request)
 
-    result = subscribe_users(request, request.form['users'][0].split(","), pagename, formatter)
+    usernames = request.form['users'][0].split(",")
+    subscribe, unsubscribe = parse_userlist(usernames)
+
+    result = subscribe_users(request, subscribe, unsubscribe, pagename, formatter)
     request.write(result)
 
     request.theme.send_footer(pagename)
     request.theme.send_closing_html()
 
-def subscribe_users(request, usernamelist, pagename, formatter):
+
+def subscribe_users(request, subscribe, unsubscribe, pagename, formatter):
     _ = request.getText
 
     if not Page(request, pagename).exists():
         return u"Page does not exist."
 
     result = []
-
-    realusers = []              # usernames that are really wiki users
+    did_match = {}
 
     # get user object - only with IDs!
     for userid in user.getUserList(request):
-        success = False
         userobj = user.User(request, userid)
+        name = userobj.name
 
-        if userobj.name in usernamelist:   # found a user
-            realusers.append(userobj.name)
-            if userobj.isSubscribedTo([pagename]):
-                success = True
-            elif not userobj.email and not userobj.jid:
-                success = False
-            elif userobj.subscribe(pagename):
-                success = True
-            if success:
-                result.append(formatter.smiley('{OK}'))
-                result.append(formatter.text(" "))
-            else:
-                result.append(formatter.smiley('{X}'))
-                result.append(formatter.text(" "))
-            result.append(formatter.url(1, Page(request, userobj.name).url(request)))
-            result.append(formatter.text(userobj.name))
-            result.append(formatter.url(0))
-            result.append(formatter.linebreak(preformatted=0))
+        matched = subscribed = False
 
-    result.extend([''.join([formatter.smiley('{X}'), formatter.text(" " + _("Not a user:") + " " + username), formatter.linebreak(preformatted=0)]) for username in usernamelist if username not in realusers])
+        for name_re in unsubscribe:
+            if re.match(name_re, name, re.U):
+                matched = did_match[name_re] = True
+                if (not userobj.isSubscribedTo([pagename]) or
+                    userobj.unsubscribe(pagename)):
+                    subscribed = False
+                break
+
+        for name_re in subscribe:
+            if re.match(name_re, name, re.U):
+                matched = did_match[name_re] = True
+                if (userobj.isSubscribedTo([pagename]) or
+                    (userobj.email or userobj.jid) and userobj.subscribe(pagename)):
+                    subscribed = True
+                break
+
+        if matched:
+            result.extend([formatter.smiley(subscribed and '{*}' or '{o}'),
+                           formatter.text(" "),
+                           formatter.url(1, Page(request, name).url(request)),
+                           formatter.text(name),
+                           formatter.url(0),
+                           formatter.linebreak(preformatted=0),
+                          ])
+
+    result.extend([''.join([formatter.smiley('{X}'),
+                            formatter.text(" " + _("Not a user:") + " " + name_re),
+                            formatter.linebreak(preformatted=0)])
+                   for name_re in subscribe + unsubscribe if name_re not in did_match])
 
     return ''.join(result)
 
+
 def execute(pagename, request):
     _ = request.getText
     if not request.user.may.admin(pagename):
@@ -94,14 +135,19 @@
     else:
         show_result(pagename, request)
 
+
 if __name__ == '__main__':
     args = sys.argv
-    if not len(args) > 1:
+    if len(args) < 2:
         print >>sys.stderr, """Subscribe users
 
-%(myname)s pagename username[,username[,username[,...]]] [URL]
+%(myname)s pagename [+|-][re:]username[,username[,username[,...]]] [URL]
 
-Subscribes the users to a page.
++username: subscribes user <username> to page <pagename>.
+-username: unsubscribes user <username> from page <pagename>.
++re:username_re: subscribes users who match <username_re> regex.
+-re:username_re: unsubscribes users who match <username_re> regex.
+
 URL is just needed for a farmconfig scenario.
 
 Example:
@@ -111,7 +157,7 @@
         raise SystemExit
 
     pagename = args[1]
-    usernames = args[2]
+    usernames = args[2].split(",")
 
     if len(args) > 3:
         request_url = args[3]
@@ -126,5 +172,7 @@
     from MoinMoin.formatter.text_plain import Formatter
     formatter = Formatter(request)
 
-    print subscribe_users(request, usernames.split(","), pagename, formatter)
+    subscribe, unsubscribe = parse_userlist(usernames)
 
+    print subscribe_users(request, subscribe, unsubscribe, pagename, formatter)
+
--- a/MoinMoin/action/SyncPages.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/SyncPages.py	Wed Feb 03 13:37:38 2010 +0100
@@ -46,8 +46,12 @@
         self.status = []
         self.rollback = set()
 
-    def log_status(self, level, message="", substitutions=(), raw_suffix=""):
+    def log_status(self, level, message=u"", substitutions=(), raw_suffix=u""):
         """ Appends the message with a given importance level to the internal log. """
+        if isinstance(message, str):
+            message = message.decode("utf-8")
+        if isinstance(raw_suffix, str):
+            raw_suffix = raw_suffix.decode("utf-8")
         self.status.append((level, message, substitutions, raw_suffix))
 
     def register_rollback(self, func):
@@ -74,17 +78,18 @@
         table = []
 
         for line in self.status:
-            if line[1]:
-                if line[2]:
-                    macro_args = [line[1]] + list(line[2])
+            level, message, substitutions, raw_suffix = line
+            if message:
+                if substitutions:
+                    macro_args = [message] + list(substitutions)
                     message = u"<<GetText2(|%s)>>" % (packLine(macro_args), )
                 else:
-                    message = u"<<GetText(%s)>>" % (line[1], )
+                    message = u"<<GetText(%s)>>" % (message, )
             else:
                 message = u""
-            table.append(table_line % {"smiley": line[0][1],
+            table.append(table_line % {"smiley": level[1],
                                        "message": message,
-                                       "raw_suffix": line[3].replace("\n", "<<BR>>")})
+                                       "raw_suffix": raw_suffix.replace("\n", "<<BR>>")})
 
         return "\n".join(table)
 
--- a/MoinMoin/action/backup.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/backup.py	Wed Feb 03 13:37:38 2010 +0100
@@ -1,12 +1,18 @@
 # -*- coding: iso-8859-1 -*-
 """
-    MoinMoin - make or restore a full backup of the wiki
+    MoinMoin - download a backup via http.
 
-    Triggering backup action will check if you are authorized to do
-    a backup and if yes, just send a
-    <siteid>-<date>--<time>.tar.<format> to you.
+    Triggering backup action will check if you are authorized to do a backup
+    and if yes, just send a <siteid>-<date>--<time>.tar.<format> to you.
+    What exactly is contained in your backup depends on your wiki's
+    configuration - please make sure you have everything you need BEFORE you
+    really need it.
 
-    @copyright: 2005 by MoinMoin:ThomasWaldmann
+    Note: there is no restore support, you need somebody having access to your
+          wiki installation via the server's file system, knowing about tar
+          and restoring your data CAREFULLY (AKA "the server admin").
+
+    @copyright: 2005-2008 by MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -15,81 +21,51 @@
 from MoinMoin import wikiutil
 from MoinMoin.support import tarfile
 
-def addFiles(path, tar, exclude):
+
+def addFiles(path, tar, exclude_func):
     """ Add files in path to tar """
     for root, dirs, files in os.walk(path):
         files.sort() # sorted page revs may compress better
         for name in files:
             path = os.path.join(root, name)
-            if exclude.search(path):
+            if exclude_func(path):
                 continue
             tar.add(path)
 
+
 def sendBackup(request):
     """ 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.emit_http_headers([
-        "Content-Type: application/octet-stream",
-        "Content-Disposition: inline; filename=\"%s\"" % filename, ])
+        'Content-Type: application/octet-stream',
+        'Content-Disposition: inline; filename="%s"' % filename, ])
 
     tar = tarfile.open(fileobj=request, mode="w|%s" % request.cfg.backup_compression)
     # allow GNU tar's longer file/pathnames
     tar.posix = False
-    exclude = re.compile("|".join(request.cfg.backup_exclude))
     for path in request.cfg.backup_include:
-        addFiles(path, tar, exclude)
+        addFiles(path, tar, request.cfg.backup_exclude)
     tar.close()
 
-def restoreBackup(request, pagename):
-    _ = request.getText
-    path = request.cfg.backup_storage_dir
-    filename = "%s.tar.%s" % (request.cfg.siteid, request.cfg.backup_compression)
-    filename = os.path.join(path, filename)
-    targetdir = request.cfg.backup_restore_target_dir
-    try:
-        tar = tarfile.open(fileobj=file(filename), mode="r|%s" % request.cfg.backup_compression)
-        # allow GNU tar's longer file/pathnames
-        tar.posix = False
-        files = []
-        dirs = []
-        for m in tar:
-            if m.isdir():
-                dirs.append("%s %s %s" % (m.name, m.size, m.mtime))
-            else:
-                files.append("%s %s %s" % (m.name, m.size, m.mtime))
-            tar.extract(m, targetdir)
-        tar.close()
-        #files = "<br>".join(files)
-        filecount = len(files)
-        dircount = len(dirs)
-        return sendMsg(request, pagename,
-            msg=_('Restored Backup: %(filename)s to target dir: %(targetdir)s.\nFiles: %(filecount)d, Directories: %(dircount)d') %
-                locals(), msgtype="info")
-    except:
-        return sendMsg(request, pagename, msg=_("Restoring backup: %(filename)s to target dir: %(targetdir)s failed.") % locals(), msgtype="info")
 
 def sendBackupForm(request, pagename):
     _ = request.getText
     request.emit_http_headers()
     request.setContentLanguage(request.lang)
-    title = _('Wiki Backup / Restore')
+    title = _('Wiki Backup')
     request.theme.send_title(title, form=request.form, pagename=pagename)
     request.write(request.formatter.startContent("content"))
 
-    request.write(_("""Some hints:
- * To restore a backup:
-  * Restoring a backup will overwrite existing data, so be careful.
-  * Rename it to <siteid>.tar.<compression> (remove the --date--time--UTC stuff).
-  * Put the backup file into the backup_storage_dir (use scp, ftp, ...).
-  * Hit the <<GetText(Restore)>> button below.
+    request.write(_("""= Downloading a backup =
 
- * To make a backup, just hit the <<GetText(Backup)>> button and save the file
-   you get to a secure place.
+Please note:
+ * Store backups in a safe and secure place - they contain sensitive information.
+ * Make sure your wiki configuration backup_* values are correct and complete.
+ * Make sure the backup file you get contains everything you need in case of problems.
+ * Make sure it is downloaded without problems.
 
-Please make sure your wiki configuration backup_* values are correct and complete.
-
-""", wiki=True))
+To get a backup, just click here:""", wiki=True))
 
     request.write("""
 <form action="%(baseurl)s/%(pagename)s" method="POST" enctype="multipart/form-data">
@@ -97,34 +73,30 @@
 <input type="hidden" name="do" value="backup">
 <input type="submit" value="%(backup_button)s">
 </form>
-
-<form action="%(baseurl)s/%(pagename)s" method="POST" enctype="multipart/form-data">
-<input type="hidden" name="action" value="backup">
-<input type="hidden" name="do" value="restore">
-<input type="submit" value="%(restore_button)s">
-</form>
 """ % {
     'baseurl': request.getScriptname(),
     'pagename': wikiutil.quoteWikinameURL(pagename),
     'backup_button': _('Backup'),
-    'restore_button': _('Restore'),
 })
 
     request.write(request.formatter.endContent())
     request.theme.send_footer(pagename)
     request.theme.send_closing_html()
 
+
 def sendMsg(request, pagename, msg, msgtype):
     from MoinMoin import Page
     request.theme.add_msg(msg, msgtype)
     return Page.Page(request, pagename).send_page()
 
+
 def backupAllowed(request):
     """ Return True if backup is allowed """
     action = __name__.split('.')[-1]
     user = request.user
     return user.valid and user.name in request.cfg.backup_users
 
+
 def execute(pagename, request):
     _ = request.getText
     if not backupAllowed(request):
@@ -134,10 +106,9 @@
     dowhat = request.form.get('do', [None])[0]
     if dowhat == 'backup':
         sendBackup(request)
-    elif dowhat == 'restore':
-        restoreBackup(request, pagename)
     elif dowhat is None:
         sendBackupForm(request, pagename)
     else:
         return sendMsg(request, pagename,
                        msg=_('Unknown backup subaction: %s.') % dowhat, msgtype="error")
+
--- a/MoinMoin/action/cache.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/cache.py	Wed Feb 03 13:37:38 2010 +0100
@@ -27,8 +27,6 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import hmac, sha
-
 from MoinMoin import log
 logging = log.getLogger(__name__)
 
@@ -39,6 +37,7 @@
 from MoinMoin import config, caching
 from MoinMoin.util import filesys
 from MoinMoin.action import AttachFile
+from MoinMoin.support.python_compatibility import hmac_new
 
 action_name = __name__.split('.')[-1]
 
@@ -87,7 +86,7 @@
     @param secret: secret for hMAC calculation (default: use secret from cfg)
     """
     if secret is None:
-        secret = request.cfg.secrets
+        secret = request.cfg.secrets['action/cache']
     if content:
         hmac_data = content
     elif itemname is not None and attachname is not None:
@@ -98,7 +97,7 @@
         raise AssertionError('cache_key called with unsupported parameters')
 
     hmac_data = hmac_data.encode('utf-8')
-    key = hmac.new(secret, hmac_data, sha).hexdigest()
+    key = hmac_new(secret, hmac_data).hexdigest()
     return key
 
 
--- a/MoinMoin/action/diff.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/diff.py	Wed Feb 03 13:37:38 2010 +0100
@@ -3,7 +3,8 @@
     MoinMoin - show diff between 2 page revisions
 
     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
-                2006-2008 MoinMoin:ThomasWaldmann
+                2006-2008 MoinMoin:ThomasWaldmann,
+                2009 MoinMoin:ReimarBauer
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -119,12 +120,12 @@
     title = f.text(title)
 
     # Revision list starts from 2...
-    if oldrev == min(revlist):
+    if oldrev <= min(revlist):
         disable_prev = u' disabled="disabled"'
     else:
         disable_prev = u''
 
-    if newrev == max(revlist):
+    if newrev >= max(revlist):
         disable_next = u' disabled="disabled"'
     else:
         disable_next = u''
--- a/MoinMoin/action/fullsearch.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/fullsearch.py	Wed Feb 03 13:37:38 2010 +0100
@@ -179,21 +179,22 @@
         sort = 'weight'
 
     # search the pages
-    from MoinMoin.search import searchPages, QueryParser
+    from MoinMoin.search import searchPages, QueryParser, QueryError
     try:
         query = QueryParser(case=case, regex=regex,
                 titlesearch=titlesearch).parse_query(needle)
-        results = searchPages(request, query, sort, mtime, historysearch)
-    except ValueError: # catch errors in the search query
+    except QueryError: # catch errors in the search query
         request.theme.add_msg(_('Your search query {{{"%s"}}} is invalid. Please refer to '
                 'HelpOnSearching for more information.', wiki=True, percent=True) % wikiutil.escape(needle), "error")
         Page(request, pagename).send_page()
         return
 
-    # directly show a single hit
-    # Note: can't work with attachment search
-    # improve if we have one...
-    if len(results.hits) == 1:
+    results = searchPages(request, query, sort, mtime, historysearch)
+
+    # directly show a single hit for title searches
+    # this is the "quick jump" functionality if you don't remember
+    # the pagename exactly, but just some parts of it
+    if titlesearch and len(results.hits) == 1:
         page = results.hits[0]
         if not page.attachment: # we did not find an attachment
             page = Page(request, page.page_name)
@@ -205,7 +206,7 @@
             url = page.url(request, querystr=querydict)
             request.http_redirect(url)
             return
-    elif not results.hits: # no hits?
+    if not results.hits: # no hits?
         f = request.formatter
         querydict = wikiutil.parseQueryString(request.query_string)
         querydict.update({'titlesearch': 0})
--- a/MoinMoin/action/info.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/info.py	Wed Feb 03 13:37:38 2010 +0100
@@ -32,8 +32,8 @@
                       f.text(_("Page size: %d") % page.size()),
                       f.paragraph(0))
 
-        import sha
-        digest = sha.new(page.get_raw_body().encode(config.charset)).hexdigest().upper()
+        from MoinMoin.support.python_compatibility import hash_new
+        digest = hash_new('sha1', page.get_raw_body().encode(config.charset)).hexdigest().upper()
         request.write(f.paragraph(1),
                       f.rawHTML('%(label)s <tt>%(value)s</tt>' % {
                           'label': _("SHA digest of this page's content is:"),
@@ -176,7 +176,7 @@
 
         div = html.DIV(id="page-history")
         div.append(html.INPUT(type="hidden", name="action", value="diff"))
-        div.append(history_table.toHTML())
+        div.append(history_table.render(method="GET"))
 
         form = html.FORM(method="GET", action="")
         form.append(div)
@@ -218,7 +218,7 @@
 
     if show_hitcounts:
         from MoinMoin.stats import hitcounts
-        request.write(hitcounts.linkto(pagename, request, 'page=' + wikiutil.url_quote_plus(pagename)))
+        request.write(hitcounts.linkto(pagename, request, 'page=' + wikiutil.url_quote(pagename)))
     elif show_general:
         general(page, pagename, request)
     else:
--- a/MoinMoin/action/login.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/login.py	Wed Feb 03 13:37:38 2010 +0100
@@ -64,13 +64,9 @@
         if islogin: # user pressed login button
             if request._login_multistage:
                 return self.handle_multistage()
-            error = []
             if hasattr(request, '_login_messages'):
                 for msg in request._login_messages:
-                    error.append('<p>')
-                    error.append(msg)
-                error = ''.join(error)
-            request.theme.add_msg(error, "error")
+                    request.theme.add_msg(msg, "error")
             return self.page.send_page()
 
         else: # show login form
--- a/MoinMoin/action/newaccount.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/newaccount.py	Wed Feb 03 13:37:38 2010 +0100
@@ -59,7 +59,7 @@
 
     pw_checker = request.cfg.password_checker
     if pw_checker:
-        pw_error = pw_checker(theuser.name, password)
+        pw_error = pw_checker(request, theuser.name, password)
         if pw_error:
             return _("Password not acceptable: %s") % pw_error
 
@@ -86,9 +86,6 @@
     # save data
     theuser.save()
 
-    if form.has_key('create_and_mail'):
-        theuser.mailAccountData()
-
     result = _("User account created! You can use this account to login now...")
     if _debug:
         result = result + util.dumpFormData(form)
@@ -155,13 +152,8 @@
     row.append(html.TD())
     td = html.TD()
     row.append(td)
-    td.append(html.INPUT(type="submit", name="create_only",
+    td.append(html.INPUT(type="submit", name="create",
                          value=_('Create Profile')))
-    if request.cfg.mail_enabled:
-        td.append(html.Text(' '))
-        td.append(html.INPUT(type="submit", name="create_and_mail",
-                             value="%s + %s" % (_('Create Profile'),
-                                                _('Email'))))
 
     return unicode(ret)
 
@@ -181,7 +173,7 @@
     _ = request.getText
     form = request.form
 
-    submitted = form.has_key('create_only') or form.has_key('create_and_mail')
+    submitted = form.has_key('create')
 
     if submitted: # user pressed create button
         request.theme.add_msg(_create_user(request), "dialog")
--- a/MoinMoin/action/quickunlink.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/quickunlink.py	Wed Feb 03 13:37:38 2010 +0100
@@ -22,5 +22,7 @@
             msg = _('Your quicklink to this page could not be removed.')
     else:
         msg = _('You need to have a quicklink to this page to remove it.')
+    if msg:
+        request.theme.add_msg(msg)
+    Page(request, pagename).send_page()
 
-    Page(request, pagename).send_page(msg=msg)
--- a/MoinMoin/action/recoverpass.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/recoverpass.py	Wed Feb 03 13:37:38 2010 +0100
@@ -173,7 +173,7 @@
             pw_checker = request.cfg.password_checker
             pw_error = None
             if pw_checker:
-                pw_error = pw_checker(name, newpass)
+                pw_error = pw_checker(request, name, newpass)
                 if pw_error:
                     msg = _("Password not acceptable: %s") % pw_error
             if not pw_error:
--- a/MoinMoin/action/revert.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/revert.py	Wed Feb 03 13:37:38 2010 +0100
@@ -4,11 +4,12 @@
 
     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
                 2006-2008 MoinMoin:ThomasWaldmann,
-                2007 MoinMoin:ReimarBauer,
-                2008 MoinMoin:JohannesBerg
+                2008 MoinMoin:JohannesBerg,
+                2007-2009 MoinMoin:ReimarBauer
     @license: GNU GPL, see COPYING for details.
 """
 from MoinMoin import wikiutil
+from MoinMoin.Page import Page
 from MoinMoin.PageEditor import PageEditor
 from MoinMoin.action import ActionBase
 
@@ -36,7 +37,7 @@
     def check_condition(self):
         """ checks valid page and rev """
         _ = self._
-        if not self.request.rev:
+        if not self.request.rev or Page(self.request, self.pagename).current_rev() == self.request.rev:
             # same string as in PageEditor...
             note = _('You were viewing the current revision of this page when you called the revert action. '
                      'If you want to revert to an older revision, first view that older revision and '
--- a/MoinMoin/action/unsubscribe.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/action/unsubscribe.py	Wed Feb 03 13:37:38 2010 +0100
@@ -16,18 +16,16 @@
         request.theme.add_msg(_("You must login to use this action: %(action)s.") % {"action": actname}, "error")
         return Page(request, pagename).send_page()
 
-    msg = None
-
     if request.user.isSubscribedTo([pagename]):
         # Try to unsubscribe
         if request.user.unsubscribe(pagename):
-            msg = _('Your subscription to this page has been removed.')
+            request.theme.add_msg(_('Your subscription to this page has been removed.'), "info")
         else:
             msg = _("Can't remove regular expression subscription!") + u' ' + \
                   _("Edit the subscription regular expressions in your settings.")
+            request.theme.add_msg(msg, "error")
     else:
         # The user is not subscribed
-        msg = _('You need to be subscribed to unsubscribe.')
+        request.theme.add_msg(_('You need to be subscribed to unsubscribe.'), "info")
+    Page(request, pagename).send_page()
 
-    Page(request, pagename).send_page(msg=msg)
-
--- a/MoinMoin/auth/_tests/test_auth.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/_tests/test_auth.py	Wed Feb 03 13:37:38 2010 +0100
@@ -6,13 +6,17 @@
     @license: GNU GPL, see COPYING for details.
 """
 
+import py.test
+py.test.skip("broken due to test Config refactoring, fix later")
+
 import StringIO, urllib
 
 from MoinMoin.server.server_wsgi import WsgiConfig
 from MoinMoin.request import request_wsgi
+from MoinMoin._tests import wikiconfig
 
 
-class TestAuth:
+class AuthTest:
     """ test misc. auth methods """
     PAGES = ['FrontPage', 'MoinMoin', 'HelpContents', 'WikiSandBox', ] # must all exist!
 
@@ -56,6 +60,8 @@
         request.user = save_user
         return request # request.status, request.headers, request.output()
 
+
+class TestNoAuth(AuthTest):
     def testNoAuth(self):
         """ run a simple request, no auth, just check if it succeeds """
         environ = self.setup_env()
@@ -87,9 +93,12 @@
         output = request.output()
         assert '</html>' in output
 
+class TestAnonSession(AuthTest):
+    class Config(wikiconfig.Config):
+        anonymous_session_lifetime = 1
+
     def testAnonSession(self):
         """ run some requests, no auth, check if anon sessions work """
-        self.config = self.TestConfig(anonymous_session_lifetime=1)
         cookie = ''
         trail_expected = []
         first = True
@@ -146,11 +155,14 @@
             trail = request.session['trail']
             assert trail == trail_expected
 
+class TestHttpAuthSession(AuthTest):
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth.http import HTTPAuth
+        auth = [HTTPAuth(autocreate=True)]
+
     def testHttpAuthSession(self):
         """ run some requests with http auth, check whether session works """
-        from MoinMoin.auth.http import HTTPAuth
         username = u'HttpAuthTestUser'
-        self.config = self.TestConfig(auth=[HTTPAuth()], user_autocreate=True)
         cookie = ''
         trail_expected = []
         first = True
@@ -206,13 +218,16 @@
             trail = request.session['trail']
             assert trail == trail_expected
 
+class TestMoinAuthSession(AuthTest):
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth import MoinAuth
+        auth = [MoinAuth()]
+
     def testMoinAuthSession(self):
         """ run some requests with MoinAuth, check whether session works """
-        from MoinMoin.auth import MoinAuth
         from MoinMoin.user import User
-        self.config = self.TestConfig(auth=[MoinAuth()])
         username = u'MoinAuthTestUser'
-        password = u'secret'
+        password = u'ßecretß'
         User(self.request, name=username, password=password).save() # create user
         trail_expected = []
         first = True
--- a/MoinMoin/auth/_tests/test_ldap_login.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/_tests/test_ldap_login.py	Wed Feb 03 13:37:38 2010 +0100
@@ -7,10 +7,11 @@
 """
 
 import py.test
+py.test.skip("Broken due to test Config refactoring")
 
 from MoinMoin._tests.ldap_testbase import LDAPTstBase, LdapEnvironment, check_environ, SLAPD_EXECUTABLE
 from MoinMoin._tests.ldap_testdata import *
-from MoinMoin._tests import nuke_user
+from MoinMoin._tests import nuke_user, wikiconfig
 
 # first check if we have python 2.4, python-ldap and slapd:
 msg = check_environ()
@@ -20,7 +21,7 @@
 
 import ldap
 
-class TestSimpleLdap(LDAPTstBase):
+class TestLDAPServer(LDAPTstBase):
     basedn = BASEDN
     rootdn = ROOTDN
     rootpw = ROOTPW
@@ -39,14 +40,23 @@
         assert 'usera' in uids
         assert 'userb' in uids
 
+class TestMoinLDAPLogin(LDAPTstBase):
+    basedn = BASEDN
+    rootdn = ROOTDN
+    rootpw = ROOTPW
+    slapd_config = SLAPD_CONFIG
+    ldif_content = LDIF_CONTENT
+
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth.ldap_login import LDAPAuth
+        server_uri = self.ldap_env.slapd.url # XXX no self
+        base_dn = self.ldap_env.basedn
+        ldap_auth1 = LDAPAuth(server_uri=server_uri, base_dn=base_dn, autocreate=True)
+        auth = [ldap_auth1, ]
+
     def testMoinLDAPLogin(self):
         """ Just try accessing the LDAP server and see if usera and userb are in LDAP. """
-        server_uri = self.ldap_env.slapd.url
-        base_dn = self.ldap_env.basedn
 
-        from MoinMoin.auth.ldap_login import LDAPAuth
-        ldap_auth1 = LDAPAuth(server_uri=server_uri, base_dn=base_dn)
-        self.config = self.TestConfig(auth=[ldap_auth1, ], user_autocreate=True)
         handle_auth = self.request.handle_auth
 
         # tests that must not authenticate:
@@ -79,6 +89,15 @@
     slapd_config = SLAPD_CONFIG
     ldif_content = LDIF_CONTENT
 
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth.ldap_login import LDAPAuth
+        from MoinMoin.auth import MoinAuth
+        server_uri = self.ldap_env.slapd.url # XXX no self
+        base_dn = self.ldap_env.basedn
+        ldap_auth = LDAPAuth(server_uri=server_uri, base_dn=base_dn, autocreate=True)
+        moin_auth = MoinAuth()
+        auth = [ldap_auth, moin_auth]
+
     def teardown_class(self):
         """ Stop slapd, remove LDAP server environment """
         #self.ldap_env.stop_slapd()  # it is already stopped
@@ -89,14 +108,6 @@
             a default password there), then try logging in via moin login using
             that default password or an empty password.
         """
-        server_uri = self.ldap_env.slapd.url
-        base_dn = self.ldap_env.basedn
-
-        from MoinMoin.auth.ldap_login import LDAPAuth
-        ldap_auth = LDAPAuth(server_uri=server_uri, base_dn=base_dn)
-        from MoinMoin.auth import MoinAuth
-        moin_auth = MoinAuth()
-        self.config = self.TestConfig(auth=[ldap_auth, moin_auth], user_autocreate=True)
 
         nuke_user(self.request, u'usera')
 
@@ -171,6 +182,18 @@
     slapd_config = SLAPD_CONFIG
     ldif_content = LDIF_CONTENT
 
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth.ldap_login import LDAPAuth
+        authlist = []
+        for ldap_env in self.ldap_envs: # XXX no self
+            server_uri = ldap_env.slapd.url
+            base_dn = ldap_env.basedn
+            ldap_auth = LDAPAuth(server_uri=server_uri, base_dn=base_dn,
+                                 autocreate=True,
+                                 timeout=1) # short timeout, faster testing
+            authlist.append(ldap_auth)
+        auth = authlist
+
     def setup_class(self):
         """ Create LDAP servers environment, start slapds """
         self.ldap_envs = []
@@ -195,16 +218,6 @@
 
     def testMoinLDAPFailOver(self):
         """ Try if it does a failover to a secondary LDAP, if the primary fails. """
-        from MoinMoin.auth.ldap_login import LDAPAuth
-        authlist = []
-        for ldap_env in self.ldap_envs:
-            server_uri = ldap_env.slapd.url
-            base_dn = ldap_env.basedn
-            ldap_auth = LDAPAuth(server_uri=server_uri, base_dn=base_dn,
-                                 timeout=1) # short timeout, faster testing
-            authlist.append(ldap_auth)
-
-        self.config = self.TestConfig(auth=authlist, user_autocreate=True)
         handle_auth = self.request.handle_auth
 
         # authenticate user (with primary slapd):
--- a/MoinMoin/auth/botbouncer.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/botbouncer.py	Wed Feb 03 13:37:38 2010 +0100
@@ -20,7 +20,7 @@
         if kw.get('multistage'):
             uid = request.session.get('botbouncer.uid', None)
             if not uid:
-                return CancelLogin()
+                return CancelLogin(None)
             openid = request.session['botbouncer.id']
             del request.session['botbouncer.id']
             del request.session['botbouncer.uid']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/cas.py	Wed Feb 03 13:37:38 2010 +0100
@@ -0,0 +1,121 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - CAS authentication
+
+    Jasig CAS (see http://www.jasig.org/cas) authentication module.
+
+    @copyright: 2009 MoinMoin:RichardLiao
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import time, re
+import urlparse
+import urllib, urllib2
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin.auth import BaseAuth
+from MoinMoin import user, wikiutil
+
+
+class PyCAS(object):
+    """A class for working with a CAS server."""
+
+    def __init__(self, server_url, renew=False, login_path='/login', logout_path='/logout',
+                 validate_path='/validate', coding='utf-8'):
+        self.server_url = server_url
+        self.renew = renew
+        self.login_path = login_path
+        self.logout_path = logout_path
+        self.validate_path = validate_path
+        self.coding = coding
+
+    def login_url(self, service):
+        """Return the login URL for the given service."""
+        url = self.server_url + self.login_path + '?service=' + urllib.quote_plus(service)
+        if self.renew:
+            url += "&renew=true"
+        return url
+
+    def logout_url(self, redirect_url=None):
+        """Return the logout URL."""
+        url = self.server_url + self.logout_path
+        if redirect_url:
+            url += '?url=' + urllib.quote_plus(redirect_url)
+        return url
+
+    def validate_url(self, service, ticket):
+        """Return the validation URL for the given service. (For CAS 1.0)"""
+        url = self.server_url + self.validate_path + '?service=' + urllib.quote_plus(service) + '&ticket=' + urllib.quote_plus(ticket)
+        if self.renew:
+            url += "&renew=true"
+        return url
+
+    def validate_ticket(self, service, ticket):
+        """Validate the given ticket against the given service."""
+        f = urllib2.urlopen(self.validate_url(service, ticket))
+        valid = f.readline()
+        valid = valid.strip() == 'yes'
+        user = f.readline().strip()
+        user = user.decode(self.coding)
+        return valid, user
+
+
+class CASAuth(BaseAuth):
+    """ handle login from CAS """
+    name = 'CAS'
+    login_inputs = ['username', 'password']
+    logout_possible = True
+
+    def __init__(self, auth_server, login_path="/login", logout_path="/logout", validate_path="/validate"):
+        BaseAuth.__init__(self)
+        self.cas = PyCAS(auth_server, login_path=login_path,
+                         validate_path=validate_path, logout_path=logout_path)
+
+    def request(self, request, user_obj, **kw):
+        ticket = request.args.get('ticket')
+        action = request.args.get("action", [])
+        logoutRequest = request.args.get('logoutRequest', [])
+        url = request.getBaseURL() + urllib.quote_plus(request.getPathinfo().encode('utf-8'))
+
+        # # handle logout request from CAS
+        # if logoutRequest:
+            # logoutRequestMatch = re.search("<samlp:SessionIndex>(.*)</samlp:SessionIndex>", logoutRequest[0])
+            # service_ticket = logoutRequestMatch.group(1)
+            # if service_ticket:
+                # # TODO: logout
+                # return self.logout(request, user_obj)
+
+        # authenticated user
+        if user_obj and user_obj.valid:
+            return user_obj, True
+
+        # anonymous
+        if not ticket and not "login" in action:
+            return user_obj, True
+
+        # valid ticket on CAS
+        if ticket:
+            valid, username = self.cas.validate_ticket(url, ticket[0])
+            if valid:
+                u = user.User(request, auth_username=username, auth_method=self.name)
+                u.valid = valid
+                # auto create user
+                u.create_or_update(True)
+                return u, True
+
+        # login
+        request.http_redirect(self.cas.login_url(url))
+
+        return user_obj, True
+
+    def logout(self, request, user_obj, **kw):
+        if self.name and user_obj and user_obj.auth_method == self.name:
+            url = request.getBaseURL() + urllib.quote_plus(request.getPathinfo().encode('utf-8'))
+            request.http_redirect(self.cas.logout_url(url))
+
+            user_obj.valid = False
+
+        return user_obj, True
+
--- a/MoinMoin/auth/http.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/http.py	Wed Feb 03 13:37:38 2010 +0100
@@ -13,6 +13,9 @@
     @license: GNU GPL, see COPYING for details.
 """
 
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
 from MoinMoin import config, user
 from MoinMoin.request import request_twisted, request_cli, request_standalone
 from MoinMoin.auth import BaseAuth
@@ -22,6 +25,10 @@
     """ authenticate via http basic/digest/ntlm auth """
     name = 'http'
 
+    def __init__(self, autocreate=False):
+        self.autocreate = autocreate
+        BaseAuth.__init__(self)
+
     def request(self, request, user_obj, **kw):
         u = None
         _ = request.getText
@@ -32,6 +39,7 @@
         if user_obj:
             return user_obj, True
 
+        logging.debug("request: %r" % request)
         # for standalone, request authorization and verify it,
         # deny access if it isn't verified
         if isinstance(request, request_standalone.Request):
@@ -40,7 +48,9 @@
             if auth:
                 auth = auth.split()[-1]
                 info = decodestring(auth).split(':', 1)
+                logging.debug("len(info) == %d" % len(info))
                 if len(info) == 2:
+                    logging.debug("username: %r" % info[0])
                     u = user.User(request, auth_username=info[0], password=info[1],
                                   auth_method=self.name, auth_attribs=[])
             if not u:
@@ -49,6 +59,7 @@
         elif isinstance(request, request_twisted.Request):
             username = request.twistd.getUser().decode(config.charset)
             password = request.twistd.getPassword().decode(config.charset)
+            logging.debug("username: %r" % username)
             # when using Twisted http auth, we use username and password from
             # the moin user profile, so both can be changed by user.
             u = user.User(request, auth_username=username, password=password,
@@ -56,8 +67,10 @@
         elif not isinstance(request, request_cli.Request):
             env = request.env
             auth_type = env.get('AUTH_TYPE', '').lower()
+            logging.debug("auth_type: %r" % auth_type)
             if auth_type in ['basic', 'digest', 'ntlm', 'negotiate', ]:
                 username = env.get('REMOTE_USER', '').decode(config.charset)
+                logging.debug("username: %r" % username)
                 if auth_type in ('ntlm', 'negotiate', ):
                     # converting to standard case so the user can even enter wrong case
                     # (added since windows does not distinguish between e.g.
@@ -67,14 +80,20 @@
                     # this "normalizes" the login name from {meier, Meier, MEIER} to Meier
                     # put a comment sign in front of next line if you don't want that:
                     username = username.title()
+                    logging.debug("processed username: %r" % username)
                 # when using http auth, we have external user name and password,
                 # we don't use the moin user profile for those attributes.
                 u = user.User(request, auth_username=username,
                               auth_method=self.name, auth_attribs=('name', 'password'))
 
-        if u:
+        logging.debug("u: %r" % u)
+        if u and self.autocreate:
+            logging.debug("autocreating user")
             u.create_or_update()
         if u and u.valid:
+            logging.debug("returning valid user %r" % u)
             return u, True # True to get other methods called, too
         else:
+            logging.debug("returning %r" % user_obj)
             return user_obj, True
+
--- a/MoinMoin/auth/interwiki.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/interwiki.py	Wed Feb 03 13:37:38 2010 +0100
@@ -20,9 +20,10 @@
     logout_possible = True
     login_inputs = ['username', 'password']
 
-    def __init__(self, trusted_wikis):
+    def __init__(self, trusted_wikis, autocreate=False):
         BaseAuth.__init__(self)
         self.trusted_wikis = trusted_wikis
+        self.autocreate = autocreate
 
     def login(self, request, user_obj, **kw):
         username = kw.get('username')
@@ -68,7 +69,8 @@
             if key not in request.cfg.user_transient_fields:
                 setattr(u, key, value)
         u.valid = True
-        u.create_or_update(True)
+        if self.autocreate:
+            u.create_or_update(True)
         logging.debug("successful interwiki auth for %r" % name)
         return ContinueLogin(u)
 
--- a/MoinMoin/auth/ldap_login.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/ldap_login.py	Wed Feb 03 13:37:38 2010 +0100
@@ -72,6 +72,7 @@
         aliasname_attribute=None, # ('displayName') ldap attribute we get the aliasname from
         email_attribute=None, # ('mail') ldap attribute we get the email address from
         email_callback=None, # called to make up email address
+        name_callback=None, # called to use a Wiki name different from the login name
         coding='utf-8', # coding used for ldap queries and result values
         timeout=10, # how long we wait for the ldap server [s]
         start_tls=0, # 0 = No, 1 = Try, 2 = Required
@@ -81,6 +82,8 @@
         tls_keyfile=None,
         tls_require_cert=0, # 0 == ldap.OPT_X_TLS_NEVER (needed for self-signed certs)
         bind_once=False, # set to True to only do one bind - useful if configured to bind as the user on the first attempt
+        autocreate=False, # set to True if you want to autocreate user profiles
+        name='ldap', # use e.g. 'ldap_pdc' and 'ldap_bdc' (or 'ldap1' and 'ldap2') if you auth against 2 ldap servers
         ):
         self.server_uri = server_uri
         self.bind_dn = bind_dn
@@ -95,6 +98,7 @@
         self.aliasname_attribute = aliasname_attribute
         self.email_attribute = email_attribute
         self.email_callback = email_callback
+        self.name_callback = name_callback
 
         self.coding = coding
         self.timeout = timeout
@@ -107,7 +111,8 @@
         self.tls_require_cert = tls_require_cert
 
         self.bind_once = bind_once
-
+        self.autocreate = autocreate
+        self.name = name
 
     def login(self, request, user_obj, **kw):
         username = kw.get('username')
@@ -217,6 +222,9 @@
                         aliasname = sn
                 aliasname = aliasname.decode(coding)
 
+                if self.name_callback:
+                    username = self.name_callback(ldap_dict)
+
                 if email:
                     u = user.User(request, auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author', ))
                     u.email = email
@@ -225,13 +233,14 @@
                 u.name = username
                 u.aliasname = aliasname
                 u.remember_me = 0 # 0 enforces cookie_lifetime config param
-                logging.debug("creating userprefs with name %r email %r alias %r" % (username, email, aliasname))
+                logging.debug("creating user object with name %r email %r alias %r" % (username, email, aliasname))
 
             except ldap.INVALID_CREDENTIALS, err:
                 logging.debug("invalid credentials (wrong password?) for dn %r (username: %r)" % (dn, username))
                 return CancelLogin(_("Invalid username or password."))
 
-            if u:
+            if u and self.autocreate:
+                logging.debug("calling create_or_update to autocreate user %r" % u.name)
                 u.create_or_update(True)
             return ContinueLogin(u)
 
--- a/MoinMoin/auth/openidrp.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/openidrp.py	Wed Feb 03 13:37:38 2010 +0100
@@ -25,12 +25,14 @@
     def __init__(self, modify_request=None,
                        update_user=None,
                        create_user=None,
-                       forced_service=None):
+                       forced_service=None,
+                       idselector_com=None):
         BaseAuth.__init__(self)
         self._modify_request = modify_request or (lambda x: None)
         self._update_user = update_user or (lambda i, u: None)
         self._create_user = create_user or (lambda i, u: None)
         self._forced_service = forced_service
+        self._idselector_com = idselector_com
         if forced_service:
             self.login_inputs = ['special_no_input']
 
@@ -193,7 +195,7 @@
 
     def _handle_associate_continuation(self, request):
         if not 'openid.id' in request.session:
-            return CancelLogin()
+            return CancelLogin(None)
 
         _ = request.getText
         username = request.form.get('username', [''])[0]
@@ -219,7 +221,7 @@
             return self._handle_name_continuation(request)
         elif oidstage == '3':
             return self._handle_associate_continuation(request)
-        return CancelLogin()
+        return CancelLogin(None)
 
     def _openid_form(self, request, form, oidhtml):
         _ = request.getText
@@ -294,5 +296,9 @@
 
     def login_hint(self, request):
         _ = request.getText
-        return _("If you do not have an account yet, you can still log in "
+        msg = u''
+        if self._idselector_com:
+            msg = self._idselector_com
+        msg += _("If you do not have an account yet, you can still log in "
                  "with your OpenID and create one during login.")
+        return msg
--- a/MoinMoin/auth/php_session.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/php_session.py	Wed Feb 03 13:37:38 2010 +0100
@@ -21,7 +21,7 @@
 
     name = 'php_session'
 
-    def __init__(self, apps=['egw'], s_path="/tmp", s_prefix="sess_"):
+    def __init__(self, apps=['egw'], s_path="/tmp", s_prefix="sess_", autocreate=False):
         """ @param apps: A list of the enabled applications. See above for
             possible keys.
             @param s_path: The path where the PHP sessions are stored.
@@ -31,6 +31,7 @@
         self.s_path = s_path
         self.s_prefix = s_prefix
         self.apps = apps
+        self.autocreate = autocreate
 
     def request(self, request, user_obj, **kw):
         def handle_egroupware(session):
@@ -72,7 +73,7 @@
                 u.email = email
                 changed = True
 
-            if u:
+            if u and self.autocreate:
                 u.create_or_update(changed)
             if u and u.valid:
                 return u, True # True to get other methods called, too
--- a/MoinMoin/auth/sslclientcert.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/auth/sslclientcert.py	Wed Feb 03 13:37:38 2010 +0100
@@ -21,13 +21,15 @@
 
     def __init__(self, authorities=None,
                  email_key=True, name_key=True,
-                 use_email=False, use_name=False):
+                 use_email=False, use_name=False,
+                 autocreate=False):
         self.use_email = use_email
         self.authorities = authorities
         self.email_key = email_key
         self.name_key = name_key
         self.use_email = use_email
         self.use_name = use_name
+        self.autocreate = autocreate
         BaseAuth.__init__(self)
 
     def request(self, request, user_obj, **kw):
@@ -87,7 +89,7 @@
             elif user_obj and user_obj.auth_method == self.name:
                 user_obj.valid = False
                 return user_obj, False
-        if u:
+        if u and self.autocreate:
             u.create_or_update(changed)
         if u and u.valid:
             return u, True
--- a/MoinMoin/caching.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/caching.py	Wed Feb 03 13:37:38 2010 +0100
@@ -3,7 +3,7 @@
     MoinMoin caching module
 
     @copyright: 2001-2004 by Juergen Hermann <jh@web.de>,
-                2006-2008 MoinMoin:ThomasWaldmann,
+                2006-2009 MoinMoin:ThomasWaldmann,
                 2008 MoinMoin:ThomasPfaff
     @license: GNU GPL, see COPYING for details.
 """
@@ -20,23 +20,21 @@
 
 
 class CacheError(Exception):
-    """ raised if we have trouble reading or writing to the cache """
+    """ raised if we have trouble locking, reading or writing """
     pass
 
 
 def get_arena_dir(request, arena, scope):
-    if scope == 'page_or_wiki': # XXX DEPRECATED, remove later
-        if isinstance(arena, str):
-            return os.path.join(request.cfg.cache_dir, request.cfg.siteid, arena)
-        else: # arena is in fact a page object
-            return arena.getPagePath('cache', check_create=1)
-    elif scope == 'item': # arena is a Page instance
+    if scope == 'item': # arena is a Page instance
         # we could move cache out of the page directory and store it to cache_dir
         return arena.getPagePath('cache', check_create=1)
     elif scope == 'wiki':
         return os.path.join(request.cfg.cache_dir, request.cfg.siteid, arena)
     elif scope == 'farm':
         return os.path.join(request.cfg.cache_dir, '__common__', arena)
+    elif scope == 'dir':
+        # arena is a specific directory, just use it
+        return arena
     return None
 
 
@@ -49,7 +47,7 @@
 
 
 class CacheEntry:
-    def __init__(self, request, arena, key, scope='page_or_wiki', do_locking=True,
+    def __init__(self, request, arena, key, scope='wiki', do_locking=True,
                  use_pickle=False, use_encode=False):
         """ init a cache entry
             @param request: the request object
@@ -60,6 +58,7 @@
                           'item' - an item local cache
                           'wiki' - a wiki local cache
                           'farm' - a cache for the whole farm
+                          'dir' - just use some specific directory
             @param do_locking: if there should be a lock, normally True
             @param use_pickle: if data should be pickled/unpickled (nice for arbitrary cache content)
             @param use_encode: if data should be encoded/decoded (nice for readable cache files)
@@ -74,13 +73,8 @@
             os.makedirs(self.arena_dir)
         self._fname = os.path.join(self.arena_dir, key)
 
-        if self.locking:
-            self.lock_dir = os.path.join(self.arena_dir, '__lock__')
-            self.rlock = lock.LazyReadLock(self.lock_dir, 60.0)
-            self.wlock = lock.LazyWriteLock(self.lock_dir, 60.0)
-
         # used by file-like api:
-        self._lock = None  # either self.rlock or self.wlock
+        self._lock = None  # either a read or a write lock
         self._fileobj = None  # open cache file object
         self._tmp_fname = None  # name of temporary file (used for write)
         self._mode = None  # mode of open file object
@@ -137,22 +131,50 @@
 
         return needsupdate
 
-    def _determine_locktype(self, mode):
-        """ return the correct lock object for a specific file access mode """
-        if self.locking:
-            if 'r' in mode:
-                lock = self.rlock
-            if 'w' in mode or 'a' in mode:
-                lock = self.wlock
+    def lock(self, mode, timeout=10.0):
+        """
+        acquire a lock for <mode> ("r" or "w").
+        we just raise a CacheError if this doesn't work.
+
+        Note:
+         * .open() calls .lock(), .close() calls .unlock() if do_locking is True.
+         * if you need to do a read-modify-write, you want to use a CacheEntry
+           with do_locking=False and manually call .lock('w') and .unlock().
+        """
+        lock_dir = os.path.join(self.arena_dir, '__lock__')
+        if 'r' in mode:
+            _lock = lock.LazyReadLock(lock_dir, 60.0)
+        elif 'w' in mode:
+            _lock = lock.LazyWriteLock(lock_dir, 60.0)
+        acquired = _lock.acquire(timeout)
+        if acquired:
+            self._lock = _lock
         else:
-            lock = None
-        return lock
+            self._lock = None
+            err = "Can't acquire %s lock in %s" % (mode, lock_dir)
+            logging.error(err)
+            raise CacheError(err)
+
+    def unlock(self):
+        """
+        release the lock.
+        """
+        if self._lock:
+            self._lock.release()
+            self._lock = None
 
     # file-like interface ----------------------------------------------------
 
     def open(self, filename=None, mode='r', bufsize=-1):
         """ open the cache for reading/writing
 
+        Typical usage:
+            try:
+                cache.open('r')  # open file, create locks
+                data = cache.read()
+            finally:
+                cache.close()  # important to close file and remove locks
+
         @param filename: must be None (default - automatically determine filename)
         @param mode: 'r' (read, default), 'w' (write)
                      Note: if mode does not include 'b' (binary), it will be
@@ -163,32 +185,32 @@
         """
         assert self._fileobj is None, 'caching: trying to open an already opened cache'
         assert filename is None, 'caching: giving a filename is not supported (yet?)'
-
-        self._lock = self._determine_locktype(mode)
+        assert 'r' in mode or 'w' in mode, 'caching: mode must contain "r" or "w"'
 
         if 'b' not in mode:
             mode += 'b'  # we want to use binary mode, ever!
         self._mode = mode  # for self.close()
 
-        if not self.locking or self.locking and self._lock.acquire(1.0):
-            try:
-                if 'r' in mode:
-                    self._fileobj = open(self._fname, mode, bufsize)
-                elif 'w' in mode:
-                    # we do not write content to old inode, but to a new file
-                    # so we don't need to lock when we just want to read the file
-                    # (at least on POSIX, this works)
-                    fd, self._tmp_fname = tempfile.mkstemp('.tmp', self.key, self.arena_dir)
-                    self._fileobj = os.fdopen(fd, mode, bufsize)
-                else:
-                    raise ValueError("caching: mode does not contain 'r' or 'w'")
-            finally:
-                if self.locking:
-                    self._lock.release()
-                    self._lock = None
-        else:
-            logging.error("Can't acquire read/write lock in %s" % self.lock_dir)
-
+        if self.locking:
+            self.lock(mode)
+        try:
+            if 'r' in mode:
+                filename = self._fname
+                self._fileobj = open(filename, mode, bufsize)
+            elif 'w' in mode:
+                # we do not write content to old inode, but to a new file
+                # so we don't need to lock when we just want to read the file
+                # (at least on POSIX, this works)
+                filename = None
+                fd, filename = tempfile.mkstemp('.tmp', self.key, self.arena_dir)
+                self._tmp_fname = filename
+                self._fileobj = os.fdopen(fd, mode, bufsize)
+        except IOError, err:
+            if 'w' in mode:
+                # IOerror for 'r' can be just a non-existing file, do not log that,
+                # but if open fails for 'w', we likely have some bigger problem:
+                logging.error(str(err))
+            raise CacheError(str(err))
 
     def read(self, size=-1):
         """ read data from cache file
@@ -207,18 +229,17 @@
 
     def close(self):
         """ close cache file (and release lock, if any) """
-        if self._fileobj:
-            self._fileobj.close()
-            self._fileobj = None
-            if 'w' in self._mode:
-                filesys.chmod(self._tmp_fname, 0666 & config.umask) # fix mode that mkstemp chose
-                # this is either atomic or happening with real locks set:
-                filesys.rename(self._tmp_fname, self._fname)
-
-        if self._lock:
+        try:
+            if self._fileobj:
+                self._fileobj.close()
+                self._fileobj = None
+                if 'w' in self._mode:
+                    filesys.chmod(self._tmp_fname, 0666 & config.umask) # fix mode that mkstemp chose
+                    # this is either atomic or happening with real locks set:
+                    filesys.rename(self._tmp_fname, self._fname)
+        finally:
             if self.locking:
-                self._lock.release()
-            self._lock = None
+                self.unlock()
 
     # ------------------------------------------------------------------------
 
@@ -264,16 +285,15 @@
             raise CacheError(str(err))
 
     def remove(self):
-        if not self.locking or self.locking and self.wlock.acquire(1.0):
+        if self.locking:
+            self.lock('w')
+        try:
             try:
-                try:
-                    os.remove(self._fname)
-                except OSError:
-                    pass
-            finally:
-                if self.locking:
-                    self.wlock.release()
-        else:
-            logging.error("Can't acquire write lock in %s" % self.lock_dir)
+                os.remove(self._fname)
+            except OSError:
+                pass
+        finally:
+            if self.locking:
+                self.unlock()
 
 
--- a/MoinMoin/config/__init__.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/config/__init__.py	Wed Feb 03 13:37:38 2010 +0100
@@ -74,8 +74,9 @@
 # Other stuff
 url_schemas = ['http', 'https', 'ftp', 'file',
                'mailto', 'nntp', 'news',
-               'ssh', 'telnet', 'irc', 'ircs', 'xmpp',
-               'webcal', 'ed2k', 'rootz',
+               'ssh', 'telnet', 'irc', 'ircs', 'xmpp', 'mumble',
+               'webcal', 'ed2k', 'apt', 'rootz',
+               'gopher',
                'notes',
               ]
 
--- a/MoinMoin/config/_tests/test_configs.py	Wed Feb 03 13:35:28 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-from MoinMoin.config.multiconfig import DefaultConfig
-
-class NoUnderlay(DefaultConfig):
-    data_underlay_dir = None
-
-_tests = [NoUnderlay, ]
-
-class TestConfigs:
-    def testConfigs(self):
-        for cls in _tests:
-            cls.data_dir = self.request.cfg.data_dir
-            # quite a bad hack to make _importPlugin succeed
-            cls('MoinMoin')
--- a/MoinMoin/config/_tests/test_multiconfig.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/config/_tests/test_multiconfig.py	Wed Feb 03 13:37:38 2010 +0100
@@ -32,7 +32,7 @@
             py.test.skip("password_checker is disabled in the configuration, not testing it")
         else:
             for pw, result in self.tests_builtin:
-                pw_error = pw_checker(self.username, pw)
+                pw_error = pw_checker(self.request, self.username, pw)
                 print "%r: %s" % (pw, pw_error)
                 assert result == (pw_error is None)
 
--- a/MoinMoin/config/multiconfig.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/config/multiconfig.py	Wed Feb 03 13:37:38 2010 +0100
@@ -4,6 +4,7 @@
 
     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
                 2005-2008 MoinMoin:ThomasWaldmann.
+                2008      MoinMoin:JohannesBerg
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -16,7 +17,7 @@
 logging = log.getLogger(__name__)
 
 from MoinMoin import config, error, util, wikiutil
-import MoinMoin.auth as authmodule
+from MoinMoin.auth import MoinAuth
 import MoinMoin.events as events
 from MoinMoin.events import PageChangedEvent, PageRenamedEvent
 from MoinMoin.events import PageDeletedEvent, PageCopiedEvent
@@ -48,12 +49,12 @@
         raise
     except IndentationError, err:
         logging.exception('Your source code / config file is not correctly indented!')
-        msg = '''IndentationError: %(err)s
+        msg = """IndentationError: %(err)s
 
-The configuration files are python modules. Therefore, whitespace is
+The configuration files are Python modules. Therefore, whitespace is
 important. Make sure that you use only spaces, no tabs are allowed here!
 You have to use four spaces at the beginning of the line mostly.
-''' % {
+""" % {
     'err': err,
 }
         raise error.ConfigurationError(msg)
@@ -68,7 +69,7 @@
     """ Return url matching regular expression
 
     Import wikis list from farmconfig on the first call and compile the
-    regexes. Later then return the cached regex list.
+    regexes. Later just return the cached regex list.
 
     @rtype: list of tuples of (name, compiled re object)
     @return: url to wiki config name matching list
@@ -124,7 +125,7 @@
         logging.info("using wiki config: %s" % os.path.abspath(module.__file__))
     except ImportError, err:
         logging.exception('Could not import.')
-        msg = '''ImportError: %(err)s
+        msg = """ImportError: %(err)s
 
 Check that the file is in the same directory as the server script. If
 it is not, you must add the path of the directory where the file is
@@ -134,13 +135,13 @@
 Check that the configuration file name is either "wikiconfig.py" or the
 module name specified in the wikis list in farmconfig.py. Note that the
 module name does not include the ".py" suffix.
-''' % {
+""" % {
     'err': err,
 }
         raise error.ConfigurationError(msg)
     except AttributeError, err:
         logging.exception('An exception occured.')
-        msg = '''AttributeError: %(err)s
+        msg = """AttributeError: %(err)s
 
 Could not find required "Config" class in "%(name)s.py".
 
@@ -148,36 +149,18 @@
 made a syntax or spelling error.
 
 Another reason for this could be a name clash. It is not possible to have
-config names like e.g. stats.py - because that colides with MoinMoin/stats/ -
+config names like e.g. stats.py - because that collides with MoinMoin/stats/ -
 have a look into your MoinMoin code directory what other names are NOT
 possible.
 
 Please check your configuration file. As an example for correct syntax,
 use the wikiconfig.py file from the distribution.
-''' % {
+""" % {
     'name': name,
     'err': err,
 }
         raise error.ConfigurationError(msg)
 
-    # postprocess configuration
-    # 'setuid' special auth method auth method can log out
-    cfg.auth_can_logout = ['setuid']
-    cfg.auth_login_inputs = []
-    found_names = []
-    for auth in cfg.auth:
-        if not auth.name:
-            raise error.ConfigurationError("Auth methods must have a name.")
-        if auth.name in found_names:
-            raise error.ConfigurationError("Auth method names must be unique.")
-        found_names.append(auth.name)
-        if auth.logout_possible and auth.name:
-            cfg.auth_can_logout.append(auth.name)
-        for input in auth.login_inputs:
-            if not input in cfg.auth_login_inputs:
-                cfg.auth_login_inputs.append(input)
-    cfg.auth_have_login = len(cfg.auth_login_inputs) > 0
-
     return cfg
 
 
@@ -221,514 +204,30 @@
     pass
 
 
-class DefaultConfig(object):
-    """ default config values
+class ConfigFunctionality(object):
+    """ Configuration base class with config class behaviour.
 
-        When adding new config attributes, PLEASE use a name with the TOPIC as prefix,
-        so it will sort naturally. E.g. use "actions_excluded", not "excluded_actions".
-
-        Also, please keep it (roughly) sorted (except if you have good reasons to group otherwise).
+        This class contains the functionality for the DefaultConfig
+        class for the benefit of the WikiConfig macro.
     """
 
-    DesktopEdition = False # True gives all local users special powers - ONLY use for MMDE style usage!
-
-    SecurityPolicy = None
-
-    acl_hierarchic = False # True to use hierarchical ACLs
-    # All acl_rights_* lines must use unicode!
-    acl_rights_default = u"Trusted:read,write,delete,revert Known:read,write,delete,revert All:read,write"
-    acl_rights_before = u""
-    acl_rights_after = u""
-    acl_rights_valid = ['read', 'write', 'delete', 'revert', 'admin']
-
-    actions_excluded = ['xmlrpc',  # we do not want wiki admins unknowingly offering xmlrpc service
-                        'MyPages',  # only works when used with a non-default SecurityPolicy (e.g. autoadmin)
-                        'CopyPage',  # has questionable behaviour regarding subpages a user can't read, but can copy
-                       ]
-    allow_xslt = False
-    antispam_master_url = "http://master.moinmo.in/?action=xmlrpc2"
-
-    auth = [authmodule.MoinAuth()]
-    # default to http and xmlrpc_applytoken to get old semantics
-    # xmlrpc_applytoken shall be removed once that code is changed
-    # to have proper session handling and use request.handle_auth()
-    auth_methods_trusted = ['http', 'xmlrpc_applytoken']
-
-    backup_compression = 'gz'
-    backup_users = []
-    backup_include = []
-    backup_exclude = [
-        r"(.+\.py(c|o)$)",
-        r"%(cache_dir)s",
-        r"%(/)spages%(/)s.+%(/)scache%(/)s[^%(/)s]+$" % {'/': os.sep},
-        r"%(/)s(edit-lock|event-log|\.DS_Store)$" % {'/': os.sep},
-        ]
-    backup_storage_dir = '/tmp'
-    backup_restore_target_dir = '/tmp'
-
-    bang_meta = True
-    caching_formats = ['text_html']
-    changed_time_fmt = '%H:%M'
-
-    # chars_{upper,lower,digits,spaces} see MoinMoin/util/chartypes.py
-
-    # if you have gdchart, add something like
-    # chart_options = {'width = 720, 'height': 540}
-    chart_options = None
-
-    config_check_enabled = False
-
-    cookie_domain = None # use '.domain.tld" for a farm with hosts in that domain
-    cookie_path = None   # use '/wikifarm" for a farm with pathes below that path
-    cookie_lifetime = 12 # 12 hours from now
-    cookie_secure = None # a secure cookie is not transmitted over unsecure connection
-                         # None = auto-enable secure cookie for https
-                         # True = ever use secure cookie
-                         # False = never use secure cookie
-
-    data_dir = './data/'
-    data_underlay_dir = './underlay/'
-
-    date_fmt = '%Y-%m-%d'
-    datetime_fmt = '%Y-%m-%d %H:%M:%S'
-
-    default_markup = 'wiki'
-    docbook_html_dir = r"/usr/share/xml/docbook/stylesheet/nwalsh/html/" # correct for debian sarge
-
-    edit_bar = ['Edit', 'Comments', 'Discussion', 'Info', 'Subscribe', 'Quicklink', 'Attachments', 'ActionsMenu']
-    editor_default = 'text' # which editor is called when nothing is specified
-    editor_force = False # force using the default editor
-    editor_ui = 'freechoice' # which editor links are shown on user interface
-    editor_quickhelp = {
-        # editor markup hints quickhelp
-        # MUST be in wiki markup, even if the help is not for the wiki parser!
-        'wiki': _(u"""\
- Emphasis:: <<Verbatim('')>>''italics''<<Verbatim('')>>; <<Verbatim(''')>>'''bold'''<<Verbatim(''')>>; <<Verbatim(''''')>>'''''bold italics'''''<<Verbatim(''''')>>; <<Verbatim('')>>''mixed ''<<Verbatim(''')>>'''''bold'''<<Verbatim(''')>> and italics''<<Verbatim('')>>; <<Verbatim(----)>> horizontal rule.
- Headings:: = Title 1 =; == Title 2 ==; === Title 3 ===; ==== Title 4 ====; ===== Title 5 =====.
- Lists:: space and one of: * bullets; 1., a., A., i., I. numbered items; 1.#n start numbering at n; space alone indents.
- Links:: <<Verbatim(JoinCapitalizedWords)>>; <<Verbatim([[target|linktext]])>>.
- Tables:: || cell text |||| cell text spanning 2 columns ||;    no trailing white space allowed after tables or titles.
-
-(!) For more help, see HelpOnEditing or SyntaxReference.
-"""),
-        'rst': _("""\
-{{{
-Emphasis: *italic* **bold** ``monospace``
-
-Headings: Heading 1  Heading 2  Heading 3
-          =========  ---------  ~~~~~~~~~
-
-Horizontal rule: ----
-
-Links: TrailingUnderscore_ `multi word with backticks`_ external_
-
-.. _external: http://external-site.example.org/foo/
-
-Lists: * bullets; 1., a. numbered items.
-}}}
-(!) For more help, see the
-[[http://docutils.sourceforge.net/docs/user/rst/quickref.html|reStructuredText Quick Reference]].
-"""),
-        'creole': _(u"""\
- Emphasis:: <<Verbatim(//)>>''italics''<<Verbatim(//)>>; <<Verbatim(**)>>'''bold'''<<Verbatim(**)>>; <<Verbatim(**//)>>'''''bold italics'''''<<Verbatim(//**)>>; <<Verbatim(//)>>''mixed ''<<Verbatim(**)>>'''''bold'''<<Verbatim(**)>> and italics''<<Verbatim(//)>>;
- Horizontal Rule:: <<Verbatim(----)>>
- Force Linebreak:: <<Verbatim(\\\\)>>
- Headings:: = Title 1 =; == Title 2 ==; === Title 3 ===; ==== Title 4 ====; ===== Title 5 =====.
- Lists:: * bullets; ** sub-bullets; # numbered items; ## numbered sub items.
- Links:: <<Verbatim([[target]])>>; <<Verbatim([[target|linktext]])>>.
- Tables:: |= header text | cell text | more cell text |;
-
-(!) For more help, see HelpOnEditing or HelpOnCreoleSyntax.
-"""),
-    }
-    edit_locking = 'warn 10' # None, 'warn <timeout mins>', 'lock <timeout mins>'
-    edit_ticketing = True
-    edit_rows = 20
-
-    hacks = {} # { 'feature1': value1, ... }
-               # Configuration for features still in development.
-               # For boolean stuff just use config like this:
-               #   hacks = { 'feature': True, ...}
-               # and in the code use:
-               #   if cfg.hacks.get('feature', False): <doit>
-               # A non-existing hack key should ever mean False, None, "", [] or {}!
-
-    history_count = (100, 200) # (default_revisions_shown, max_revisions_shown)
-
-    hosts_deny = []
-
-    html_head = ''
-    html_head_queries = '''<meta name="robots" content="noindex,nofollow">\n'''
-    html_head_posts   = '''<meta name="robots" content="noindex,nofollow">\n'''
-    html_head_index   = '''<meta name="robots" content="index,follow">\n'''
-    html_head_normal  = '''<meta name="robots" content="index,nofollow">\n'''
-    html_pagetitle = None
-
-    interwikiname = None # our own interwikiname. choose wisely and never change!
-    interwiki_preferred = [] # list of wiki names to show at top of interwiki list
-
-    language_default = 'en'
-    language_ignore_browser = False # ignore browser settings, use language_default
-                                    # or user prefs
-
-    logo_string = None # can be either just some text or a piece of html shown as "logo"
-
-    log_reverse_dns_lookups = True  # if we do reverse dns lookups for logging hostnames
-                                    # instead of just IPs
-    log_timing = False # log infos about timing of actions, good to analyze load conditions
-
-    mail_from = None # u'Juergen Wiki <noreply@jhwiki.org>'
-    mail_login = None # "user pwd" if you need to use SMTP AUTH when using your mail server
-    mail_smarthost = None # your SMTP mail server
-    mail_sendmail = None # "/usr/sbin/sendmail -t -i" to not use SMTP, but sendmail
-
-    mail_import_secret = "" # a shared secret also known to the mail importer xmlrpc script
-    mail_import_subpage_template = u"$from-$date-$subject" # used for mail import
-    mail_import_pagename_search = ['subject', 'to', ] # where to look for target pagename (and in which order)
-    mail_import_pagename_envelope = u"%s" # use u"+ %s/" to add "+ " and "/" automatically
-    mail_import_pagename_regex = r'\[\[([^\]]*)\]\]' # how to find/extract the pagename from the subject
-    mail_import_wiki_addrs = [] # the e-mail addresses for e-mails that should go into the wiki
-
-    # some dangerous mimetypes (we don't use "content-disposition: inline" for them when a user
-    # downloads such attachments, because the browser might execute e.g. Javascript contained
-    # in the HTML and steal your moin session cookie or do other nasty stuff)
-    mimetypes_xss_protect = [
-        'text/html',
-        'application/x-shockwave-flash',
-        'application/xhtml+xml',
-    ]
-
-    mimetypes_embed = [
-        'application/x-dvi',
-        'application/postscript',
-        'application/pdf',
-        'application/ogg',
-        'application/vnd.visio',
-        'image/x-ms-bmp',
-        'image/svg+xml',
-        'image/tiff',
-        'image/x-photoshop',
-        'audio/mpeg',
-        'audio/midi',
-        'audio/x-wav',
-        'video/fli',
-        'video/mpeg',
-        'video/quicktime',
-        'video/x-msvideo',
-        'chemical/x-pdb',
-        'x-world/x-vrml',
-    ]
-
-
-    navi_bar = [u'RecentChanges', u'FindPage', u'HelpContents', ]
-    nonexist_qm = False
-
-    notification_bot_uri = None # uri of the jabber bot
-
-    # OpenID server support
-    openid_server_enabled = False
-    openid_server_restricted_users_group = None
-    openid_server_enable_user = False
-
-    # actions to exclude from packagepages action
-    packagepages_actions_excluded = ['setthemename', 'copythemefile', 'installplugin', 'renamepage', 'deletepage', 'delattachment']
-
-    page_credits = [
-        # Feel free to add other credits, but PLEASE do NOT change or remove
-        # the following links - you help us by keeping them "as is":
-        '<a href="http://moinmo.in/" title="This site uses the MoinMoin Wiki software.">MoinMoin Powered</a>',
-        '<a href="http://moinmo.in/Python" title="MoinMoin is written in Python.">Python Powered</a>',
-
-        # Optional credits:
-        # if you think it can be maybe misunderstood as applying to content or topic of your wiki,
-        # feel free to remove this one:
-        '<a href="http://moinmo.in/GPL" title="MoinMoin is GPL licensed.">GPL licensed</a>',
-
-        # if you don't need/want to check the html output, feel free to remove this one:
-        '<a href="http://validator.w3.org/check?uri=referer" title="Click here to validate this page.">Valid HTML 4.01</a>',
-        ]
-
-    # you can put some pieces of html at specific places into the theme output:
-    page_footer1 = ''
-    page_footer2 = ''
-    page_header1 = ''
-    page_header2 = ''
-
-    page_front_page = u'HelpOnLanguages' # this will make people choose a sane config
-    page_local_spelling_words = u'LocalSpellingWords'
-
-    # the following regexes should match the complete name when used in free text
-    # the group 'all' shall match all, while the group 'key' shall match the key only
-    # e.g. CategoryFoo -> group 'all' ==  CategoryFoo, group 'key' == Foo
-    # moin's code will add ^ / $ at beginning / end when needed
-    page_category_regex =  ur'(?P<all>Category(?P<key>(?!Template)\S+))'
-    page_dict_regex = ur'(?P<all>(?P<key>\S+)Dict)'
-    page_group_regex = ur'(?P<all>(?P<key>\S+)Group)'
-    page_template_regex = ur'(?P<all>(?P<key>\S+)Template)'
-
-    page_license_enabled = False
-    page_license_page = u'WikiLicense'
-
-    # These icons will show in this order in the iconbar, unless they
-    # are not relevant, e.g email icon when the wiki is not configured
-    # for email.
-    page_iconbar = ["up", "edit", "view", "diff", "info", "subscribe", "raw", "print", ]
-
-    # Standard buttons in the iconbar
-    page_icons_table = {
-        # key           pagekey, querystr dict, title, icon-key
-        'diff':        ('page', {'action': 'diff'}, _("Diffs"), "diff"),
-        'info':        ('page', {'action': 'info'}, _("Info"), "info"),
-        'edit':        ('page', {'action': 'edit'}, _("Edit"), "edit"),
-        'unsubscribe': ('page', {'action': 'unsubscribe'}, _("UnSubscribe"), "unsubscribe"),
-        'subscribe':   ('page', {'action': 'subscribe'}, _("Subscribe"), "subscribe"),
-        'raw':         ('page', {'action': 'raw'}, _("Raw"), "raw"),
-        'xml':         ('page', {'action': 'show', 'mimetype': 'text/xml'}, _("XML"), "xml"),
-        'print':       ('page', {'action': 'print'}, _("Print"), "print"),
-        'view':        ('page', {}, _("View"), "view"),
-        'up':          ('page_parent_page', {}, _("Up"), "up"),
-        }
-
-
-    def password_checker(username, password):
-        """ Check if a password is secure enough.
-            We use a built-in check to get rid of the worst passwords.
-
-            We do NOT use cracklib / python-crack here any more because it is
-            not thread-safe (we experienced segmentation faults when using it).
-
-            If you don't want to check passwords, use password_checker = None.
-
-            @return: None if there is no problem with the password,
-                     some string with an error msg, if the password is problematic.
-        """
-
-        try:
-            # in any case, do a very simple built-in check to avoid the worst passwords
-            if len(password) < 6:
-                raise ValueError("Password too short.")
-            if len(set(password)) < 4:
-                raise ValueError("Password has not enough different characters.")
-
-            username_lower = username.lower()
-            password_lower = password.lower()
-            if username in password or password in username or \
-               username_lower in password_lower or password_lower in username_lower:
-                raise ValueError("Password too easy (containment).")
-
-            keyboards = (ur"`1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./", # US kbd
-                         ur"^1234567890ߴqwertzuiop+asdfghjkl#yxcvbnm,.-", # german kbd
-                        ) # add more keyboards!
-            for kbd in keyboards:
-                rev_kbd = kbd[::-1]
-                if password in kbd or password in rev_kbd or \
-                   password_lower in kbd or password_lower in rev_kbd:
-                    raise ValueError("Password too easy (kbd sequence)")
-            return None
-        except ValueError, err:
-            return str(err)
-
-    password_checker = staticmethod(password_checker)
-
-    quicklinks_default = [] # preload user quicklinks with this page list
-
-    refresh = None # (minimum_delay, type), e.g.: (2, 'internal')
-    rss_cache = 60 # suggested caching time for RecentChanges RSS, in seconds
-
-    search_results_per_page = 25
-
-    session_handler = session.DefaultSessionHandler()
-    session_id_handler = session.MoinCookieSessionIDHandler()
-
-    secrets = None  # if wiki admin does not set it, will get calculated from some cfg values
-
-    shared_intermap = None # can be string or list of strings (filenames)
-
-    show_hosts = True # show hostnames on RecentChanges / info/history action
-    show_interwiki = False # show our interwiki name (usually in front of the page name)
-    show_names = True # show editor names on RecentChanges / info/history action
-    show_section_numbers = 0 # enumerate sections (headlines) by default?
-    show_timings = False # show some timing stats (usually in the footer)
-    show_version = False # show moin version info / (C) (depends on theme)
-
-    sistersites = [
-        #('Self', 'http://localhost:8080/?action=sisterpages'),
-        #('EmacsWiki', 'http://www.emacswiki.org/cgi-bin/test?action=sisterpages'),
-        #('JspWiki', 'http://www.jspwiki.org/SisterSites.jsp'),
-    ] # list of (sistersitename, sisterpagelistfetchurl)
-
-    siteid = 'default'
-    sitename = u'Untitled Wiki' # Wiki identity
-
-    stylesheets = [] # list of tuples (media, csshref) to insert after theme css, before user css
-
-    _subscribable_events = None # A list of event types that user can subscribe to
-    subscribed_pages_default = [] # preload user subscribed pages with this page list
-    email_subscribed_events_default = [
-        PageChangedEvent.__name__,
-        PageRenamedEvent.__name__,
-        PageDeletedEvent.__name__,
-        PageCopiedEvent.__name__,
-        PageRevertedEvent.__name__,
-        FileAttachedEvent.__name__,
-    ]
-    jabber_subscribed_events_default = []
-
-    superuser = [] # list of unicode user names that have super powers :)
-
-    supplementation_page = False # use supplementation pages (show a link in the theme)?
-    supplementation_page_name = u'Discussion' # name of suppl. subpage
-    supplementation_page_template = u'DiscussionTemplate' # name of template used to create suppl. pages
-
-    surge_action_limits = {# allow max. <count> <action> requests per <dt> secs
-        # action: (count, dt)
-        'all': (30, 30), # all requests (except cache/AttachFile action) count for this limit
-        'default': (30, 60), # default limit for actions without a specific limit
-        'show': (30, 60),
-        'recall': (10, 120),
-        'raw': (20, 40),  # some people use this for css
-        'diff': (30, 60),
-        'fullsearch': (10, 120),
-        'edit': (30, 300), # can be lowered after making preview different from edit
-        'rss_rc': (1, 60),
-        # The following actions are often used for images - to avoid pages with lots of images
-        # (like photo galleries) triggering surge protection, we assign rather high limits:
-        'AttachFile': (90, 60),
-        'cache': (600, 30), # cache action is very cheap/efficient
-    }
-    surge_lockout_time = 3600 # secs you get locked out when you ignore warnings
-
-    textchas = None # a data structure with site-specific questions/answers, see HelpOnTextChas
-    textchas_disabled_group = None # e.g. u'NoTextChasGroup' if you are a member of this group, you don't get textchas
-
-    theme_default = 'modern'
-    theme_force = False
-
-    traceback_show = True # if True, tracebacks are displayed in the web browser
-    traceback_log_dir = None # if set to a directory path, tracebacks are written to files there
-
-    trail_size = 5 # number of recently visited pagenames shown in the trail display
-    tz_offset = 0.0 # default time zone offset in hours from UTC
-
-    # a regex of HTTP_USER_AGENTS that should be excluded from logging
-    # and receive a FORBIDDEN for anything except viewing a page
-    # list must not contain 'java' because of twikidraw wanting to save drawing uses this useragent
-    ua_spiders = ('archiver|cfetch|charlotte|crawler|curl|gigabot|googlebot|heritrix|holmes|htdig|httrack|httpunit|'
-                  'intelix|jeeves|larbin|leech|libwww-perl|linkbot|linkmap|linkwalk|litefinder|mercator|'
-                  'microsoft.url.control|mirror| mj12bot|msnbot|msrbot|neomo|nutbot|omniexplorer|puf|robot|scooter|seekbot|'
-                  'sherlock|slurp|sitecheck|snoopy|spider|teleport|twiceler|voilabot|voyager|webreaper|wget|yeti')
-
-    unzip_single_file_size = 2.0 * 1000 ** 2
-    unzip_attachments_space = 200.0 * 1000 ** 2
-    unzip_attachments_count = 101 # 1 zip file + 100 files contained in it
-
-    url_mappings = {}
-
-    # url_prefix is DEPRECATED and not used any more by the code.
-    # it confused many people by its name and default value of '/wiki' to the
-    # wrong conclusion that it is the url of the wiki (the dynamic) stuff,
-    # but it was used to address the static stuff (images, css, js).
-    # Thus we use the more clear url_prefix_static ['/moin_staticVVV'] setting now.
-    # For a limited time, we still look at url_prefix - if it is not None, we
-    # copy the value to url_prefix_static to ease transition.
-    url_prefix = None
-
-    # includes the moin version number, so we can have a unlimited cache lifetime
-    # for the static stuff. if stuff changes on version upgrade, url will change
-    # immediately and we have no problem with stale caches.
-    url_prefix_static = config.url_prefix_static
-    url_prefix_local = None # if None, use same value as url_prefix_static.
-                            # must be same site as wiki engine (for e.g. JS permissions)
-
-    # we could prefix actions to be able to exclude them by robots.txt:
-    #url_prefix_action = 'action' # no leading or trailing '/'
-    url_prefix_action = None # compatiblity
-
-    # allow disabling certain userpreferences plugins
-    userprefs_disabled = []
-
-    user_autocreate = False # do we auto-create user profiles
-    user_email_unique = True # do we check whether a user's email is unique?
-    user_jid_unique = True # do we check whether a user's email is unique?
-
-    user_homewiki = u'Self' # interwiki name for where user homepages are located
-
-    user_checkbox_fields = [
-        ('mailto_author', lambda _: _('Publish my email (not my wiki homepage) in author info')),
-        ('edit_on_doubleclick', lambda _: _('Open editor on double click')),
-        ('remember_last_visit', lambda _: _('After login, jump to last visited page')),
-        ('show_comments', lambda _: _('Show comment sections')),
-        ('show_nonexist_qm', lambda _: _('Show question mark for non-existing pagelinks')),
-        ('show_page_trail', lambda _: _('Show page trail')),
-        ('show_toolbar', lambda _: _('Show icon toolbar')),
-        ('show_topbottom', lambda _: _('Show top/bottom links in headings')),
-        ('show_fancy_diff', lambda _: _('Show fancy diffs')),
-        ('wikiname_add_spaces', lambda _: _('Add spaces to displayed wiki names')),
-        ('remember_me', lambda _: _('Remember login information')),
-
-        ('disabled', lambda _: _('Disable this account forever')),
-        # if an account is disabled, it may be used for looking up
-        # id -> username for page info and recent changes, but it
-        # is not usable for the user any more:
-    ]
-
-    user_checkbox_defaults = {'mailto_author':       0,
-                              'edit_on_doubleclick': 0,
-                              'remember_last_visit': 0,
-                              'show_comments':       0,
-                              'show_nonexist_qm':    nonexist_qm,
-                              'show_page_trail':     1,
-                              'show_toolbar':        1,
-                              'show_topbottom':      0,
-                              'show_fancy_diff':     1,
-                              'wikiname_add_spaces': 0,
-                              'remember_me':         1,
-                             }
-
-    # don't let the user change those
-    # user_checkbox_disable = ['disabled']
-    user_checkbox_disable = []
-
-    # remove those checkboxes:
-    #user_checkbox_remove = ['edit_on_doubleclick', 'show_nonexist_qm', 'show_toolbar', 'show_topbottom',
-    #                        'show_fancy_diff', 'wikiname_add_spaces', 'remember_me', 'disabled',]
-    user_checkbox_remove = []
-
-    user_form_fields = [
-        ('name', _('Name'), "text", "36", _("(Use FirstnameLastname)")),
-        ('aliasname', _('Alias-Name'), "text", "36", ''),
-        ('email', _('Email'), "text", "36", ''),
-        ('jid', _('Jabber ID'), "text", "36", ''),
-        ('css_url', _('User CSS URL'), "text", "40", _('(Leave it empty for disabling user CSS)')),
-        ('edit_rows', _('Editor size'), "text", "3", ''),
-    ]
-
-    user_form_defaults = {# key: default - do NOT remove keys from here!
-        'name': '',
-        'aliasname': '',
-        'password': '',
-        'password2': '',
-        'email': '',
-        'jid': '',
-        'css_url': '',
-        'edit_rows': "20",
-    }
-
-    # don't let the user change those, but show them:
-    #user_form_disable = ['name', 'aliasname', 'email',]
-    user_form_disable = []
-
-    # remove those completely:
-    #user_form_remove = ['password', 'password2', 'css_url', 'logout', 'create', 'account_sendmail',]
-    user_form_remove = []
-
-    # attributes we do NOT save to the userpref file
-    user_transient_fields = ['id', 'valid', 'may', 'auth_username', 'password', 'password2', 'auth_method', 'auth_attribs', ]
-
-    xapian_search = False
-    xapian_index_dir = None
-    xapian_stemming = False
-    xapian_index_history = False
+    # attributes of this class that should not be shown
+    # in the WikiConfig() macro.
+    cfg_mtime = None
+    siteid = None
+    cache = None
+    mail_enabled = None
+    jabber_enabled = None
+    auth_can_logout = None
+    auth_have_login = None
+    auth_login_inputs = None
+    _site_plugin_lists = None
+    _iwid = None
+    _iwid_full = None
+    xapian_searchers = None
+    moinmoin_dir = None
+    # will be lazily loaded by interwiki code when needed (?)
+    shared_intermap_files = None
 
     def __init__(self, siteid):
         """ Init Config instance """
@@ -766,7 +265,7 @@
         self.cache.page_group_regexact = re.compile(u'^%s$' % self.page_group_regex, re.UNICODE)
         self.cache.page_template_regexact = re.compile(u'^%s$' % self.page_template_regex, re.UNICODE)
 
-        self.cache.ua_spiders = self.ua_spiders and re.compile(self.ua_spiders, re.I)
+        self.cache.ua_spiders = self.ua_spiders and re.compile(self.ua_spiders, re.IGNORECASE)
 
         self._check_directories()
 
@@ -802,13 +301,29 @@
 
         # post process
 
+        # 'setuid' special auth method auth method can log out
+        self.auth_can_logout = ['setuid']
+        self.auth_login_inputs = []
+        found_names = []
+        for auth in self.auth:
+            if not auth.name:
+                raise error.ConfigurationError("Auth methods must have a name.")
+            if auth.name in found_names:
+                raise error.ConfigurationError("Auth method names must be unique.")
+            found_names.append(auth.name)
+            if auth.logout_possible and auth.name:
+                self.auth_can_logout.append(auth.name)
+            for input in auth.login_inputs:
+                if not input in self.auth_login_inputs:
+                    self.auth_login_inputs.append(input)
+        self.auth_have_login = len(self.auth_login_inputs) > 0
+
         # internal dict for plugin `modules' lists
         self._site_plugin_lists = {}
 
         # we replace any string placeholders with config values
         # e.g u'%(page_front_page)s' % self
         self.navi_bar = [elem % self for elem in self.navi_bar]
-        self.backup_exclude = [elem % self for elem in self.backup_exclude]
 
         # check if python-xapian is installed
         if self.xapian_search:
@@ -823,27 +338,16 @@
 
         # 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.mail_enabled = self.mail_enabled and True or False
 
         # check if jabber bot is available and set flag:
         self.jabber_enabled = self.notification_bot_uri is not None
 
         # if we are to use the jabber bot, instantiate a server object for future use
         if self.jabber_enabled:
-
-            errmsg = "You must set a (long) secret string to send notifications!"
-            try:
-                if not self.secret:
-                    raise error.ConfigurationError(errmsg)
-            except AttributeError, err:
-                raise error.ConfigurationError(errmsg)
-
             from xmlrpclib import Server
             self.notification_server = Server(self.notification_bot_uri, )
 
-        if self.secrets is None:  # Note: this is 'secrets' (with s at the end), not 'secret' (as above)
-                                  # This stuff is already cleaned up in 1.8 repo...
-            self.secrets = self.calc_secrets()
-
         # Cache variables for the properties below
         self._iwid = self._iwid_full = self._meta_dict = None
 
@@ -851,9 +355,6 @@
         self.cache.acl_rights_default = AccessControlList(self, [self.acl_rights_default])
         self.cache.acl_rights_after = AccessControlList(self, [self.acl_rights_after])
 
-        if self.url_prefix is not None: # remove this code when url_prefix setting is removed
-            self.url_prefix_static = self.url_prefix
-
         action_prefix = self.url_prefix_action
         if action_prefix is not None and action_prefix.endswith('/'): # make sure there is no trailing '/'
             self.url_prefix_action = action_prefix[:-1]
@@ -861,6 +362,36 @@
         if self.url_prefix_local is None:
             self.url_prefix_local = self.url_prefix_static
 
+        if self.url_prefix_fckeditor is None:
+            self.url_prefix_fckeditor = self.url_prefix_local + '/applets/FCKeditor'
+
+        if self.secrets is None:  # admin did not setup a real secret, so make up something
+            self.secrets = self.calc_secrets()
+
+        secret_key_names = ['action/cache', 'wikiutil/tickets', 'xmlrpc/ProcessMail', 'xmlrpc/RemoteScript', ]
+        if self.jabber_enabled:
+            secret_key_names.append('jabberbot')
+
+        secret_min_length = 10
+        if isinstance(self.secrets, str):
+            if len(self.secrets) < secret_min_length:
+                raise error.ConfigurationError("The secrets = '...' wiki config setting is a way too short string (minimum length is %d chars)!" % (
+                    secret_min_length))
+            # for lazy people: set all required secrets to same value
+            secrets = {}
+            for key in secret_key_names:
+                secrets[key] = self.secrets
+            self.secrets = secrets
+
+        # we check if we have all secrets we need and that they have minimum length
+        for secret_key_name in secret_key_names:
+            try:
+                secret = self.secrets[secret_key_name]
+                if len(secret) < secret_min_length:
+                    raise ValueError
+            except (KeyError, ValueError):
+                raise error.ConfigurationError("You must set a (at least %d chars long) secret string for secrets['%s']!" % (
+                    secret_min_length, secret_key_name))
 
     def calc_secrets(self):
         """ make up some 'secret' using some config values """
@@ -875,9 +406,10 @@
                 secret += repr(var)
         return secret
 
+    _meta_dict = None
     def load_meta_dict(self):
         """ The meta_dict contains meta data about the wiki instance. """
-        if getattr(self, "_meta_dict", None) is None:
+        if self._meta_dict is None:
             self._meta_dict = wikiutil.MetaDict(os.path.join(self.data_dir, 'meta'), self.cache_dir)
         return self._meta_dict
     meta_dict = property(load_meta_dict)
@@ -892,25 +424,13 @@
     iwid = make_iwid_property("_iwid")
     iwid_full = make_iwid_property("_iwid_full")
 
-    # lazily load a list of events a user can subscribe to
-    def make_subscribable_events_prop():
-        def getter(self):
-            if getattr(self, "_subscribable_events", None) is None:
-                self._subscribable_events = events.get_subscribable_events()
-            return getattr(self, "_subscribable_events")
-
-        def setter(self, new_events):
-            self._subscribable_events = new_events
-
-        return property(getter, setter)
-    subscribable_events = make_subscribable_events_prop()
-
     # lazily create a list of event handlers
+    _event_handlers = None
     def make_event_handlers_prop():
         def getter(self):
-            if getattr(self, "_event_handlers", None) is None:
+            if self._event_handlers is None:
                 self._event_handlers = events.get_handlers(self)
-            return getattr(self, "_event_handlers")
+            return self._event_handlers
 
         def setter(self, new_handlers):
             self._event_handlers = new_handlers
@@ -975,13 +495,13 @@
         config files.
         """
         charset = 'utf-8'
-        message = u'''
+        message = u"""
 "%(name)s" configuration variable is a string, but should be
 unicode. Use %(name)s = u"value" syntax for unicode variables.
 
 Also check your "-*- coding -*-" line at the top of your configuration
 file. It should match the actual charset of the configuration file.
-'''
+"""
 
         decode_names = (
             'sitename', 'interwikiname', 'user_homewiki', 'logo_string', 'navi_bar',
@@ -1028,7 +548,7 @@
 
             path_pages = os.path.join(path, "pages")
             if not (os.path.isdir(path_pages) and os.access(path_pages, mode)):
-                msg = '''
+                msg = """
 %(attr)s "%(path)s" does not exist, or has incorrect ownership or
 permissions.
 
@@ -1038,50 +558,60 @@
 
 It is recommended to use absolute paths and not relative paths. Check
 also the spelling of the directory name.
-''' % {'attr': attr, 'path': path, }
+""" % {'attr': attr, 'path': path, }
                 raise error.ConfigurationError(msg)
 
     def _loadPluginModule(self):
-        """ import plugin module under configname.plugin
+        """
+        import all plugin modules
 
         To be able to import plugin from arbitrary path, we have to load
         the base package once using imp.load_module. Later, we can use
         standard __import__ call to load plugins in this package.
 
-        Since each wiki has unique plugins, we load the plugin package
-        under the wiki configuration module, named self.siteid.
+        Since each configured plugin path has unique plugins, we load the
+        plugin packages as "moin_plugin_<sha1(path)>.plugin".
         """
         import imp
+        from MoinMoin.support.python_compatibility import hash_new
 
-        name = self.siteid + '.plugin'
+        plugin_dirs = [self.plugin_dir] + self.plugin_dirs
+        self._plugin_modules = []
+
         try:
             # Lock other threads while we check and import
             imp.acquire_lock()
             try:
-                # If the module is not loaded, try to load it
-                if not name in sys.modules:
-                    # Find module on disk and try to load - slow!
-                    plugin_parent_dir = os.path.abspath(os.path.join(self.plugin_dir, '..'))
-                    fp, path, info = imp.find_module('plugin', [plugin_parent_dir])
-                    try:
-                        # Load the module and set in sys.modules
-                        module = imp.load_module(name, fp, path, info)
-                        sys.modules[self.siteid].plugin = module
-                    finally:
-                        # Make sure fp is closed properly
-                        if fp:
-                            fp.close()
+                for pdir in plugin_dirs:
+                    csum = 'p_%s' % hash_new('sha1', pdir).hexdigest()
+                    modname = '%s.%s' % (self.siteid, csum)
+                    # If the module is not loaded, try to load it
+                    if not modname in sys.modules:
+                        # Find module on disk and try to load - slow!
+                        abspath = os.path.abspath(pdir)
+                        parent_dir, pname = os.path.split(abspath)
+                        fp, path, info = imp.find_module(pname, [parent_dir])
+                        try:
+                            # Load the module and set in sys.modules
+                            module = imp.load_module(modname, fp, path, info)
+                            setattr(sys.modules[self.siteid], 'csum', module)
+                        finally:
+                            # Make sure fp is closed properly
+                            if fp:
+                                fp.close()
+                    if modname not in self._plugin_modules:
+                        self._plugin_modules.append(modname)
             finally:
                 imp.release_lock()
         except ImportError, err:
-            msg = '''
-Could not import plugin package "%(path)s/plugin" because of ImportError:
+            msg = """
+Could not import plugin package "%(path)s" because of ImportError:
 %(err)s.
 
 Make sure your data directory path is correct, check permissions, and
 that the data/plugin directory has an __init__.py file.
-''' % {
-    'path': self.data_dir,
+""" % {
+    'path': pdir,
     'err': str(err),
 }
             raise error.ConfigurationError(msg)
@@ -1101,6 +631,609 @@
         """ Make it possible to access a config object like a dict """
         return getattr(self, item)
 
+
+class DefaultConfig(ConfigFunctionality):
+    """ Configuration base class with default config values
+        (added below)
+    """
+    # Do not add anything into this class. Functionality must
+    # be added above to avoid having the methods show up in
+    # the WikiConfig macro. Settings must be added below to
+    # the options dictionary.
+
+
+def _default_password_checker(cfg, request, username, password):
+    """ Check if a password is secure enough.
+        We use a built-in check to get rid of the worst passwords.
+
+        We do NOT use cracklib / python-crack here any more because it is
+        not thread-safe (we experienced segmentation faults when using it).
+
+        If you don't want to check passwords, use password_checker = None.
+
+        @return: None if there is no problem with the password,
+                 some unicode object with an error msg, if the password is problematic.
+    """
+    _ = request.getText
+    # in any case, do a very simple built-in check to avoid the worst passwords
+    if len(password) < 6:
+        return _("Password is too short.")
+    if len(set(password)) < 4:
+        return _("Password has not enough different characters.")
+
+    username_lower = username.lower()
+    password_lower = password.lower()
+    if username in password or password in username or \
+       username_lower in password_lower or password_lower in username_lower:
+        return _("Password is too easy (password contains name or name contains password).")
+
+    keyboards = (ur"`1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./", # US kbd
+                 ur"^1234567890ߴqwertzuiop+asdfghjkl#yxcvbnm,.-", # german kbd
+                ) # add more keyboards!
+    for kbd in keyboards:
+        rev_kbd = kbd[::-1]
+        if password in kbd or password in rev_kbd or \
+           password_lower in kbd or password_lower in rev_kbd:
+            return _("Password is too easy (keyboard sequence).")
+    return None
+
+
+class DefaultExpression(object):
+    def __init__(self, exprstr):
+        self.text = exprstr
+        self.value = eval(exprstr)
+
+
+#
+# Options that are not prefixed automatically with their
+# group name, see below (at the options dict) for more
+# information on the layout of this structure.
+#
+options_no_group_name = {
+  # ==========================================================================
+  'session': ('Session settings', "Session-related settings, see HelpOnSessions.", (
+    ('session_handler', DefaultExpression('session.DefaultSessionHandler()'),
+     "See HelpOnSessions."),
+    ('session_id_handler', DefaultExpression('session.MoinCookieSessionIDHandler()'),
+     "Only used by the DefaultSessionHandler, see HelpOnSessions."),
+    ('cookie_secure', None,
+     'Use secure cookie. (None = auto-enable secure cookie for https, True = ever use secure cookie, False = never use secure cookie).'),
+    ('cookie_domain', None,
+     'Domain used in the session cookie. (None = do not specify domain).'),
+    ('cookie_path', None,
+     'Path used in the session cookie (None = auto-detect).'),
+    ('cookie_lifetime', 12,
+     'Session lifetime [h] of logged-in users (see HelpOnSessions for details).'),
+    ('anonymous_session_lifetime', None,
+     'Session lifetime [h] of users who are not logged in (None = disable anon sessions).'),
+  )),
+  # ==========================================================================
+  'auth': ('Authentication / Authorization / Security settings', None, (
+    ('superuser', [],
+     "List of trusted user names with wiki system administration super powers (not to be confused with ACL admin rights!). Used for e.g. software installation, language installation via SystemPagesSetup and more. See also HelpOnSuperUser."),
+    ('auth', DefaultExpression('[MoinAuth()]'),
+     "list of auth objects, to be called in this order (see HelpOnAuthentication)"),
+    ('auth_methods_trusted', ['http', 'xmlrpc_applytoken'],
+     'authentication methods for which users should be included in the special "Trusted" ACL group.'),
+    ('secrets', None, """Either a long shared secret string used for multiple purposes or a dict {"purpose": "longsecretstring", ...} for setting up different shared secrets for different purposes. If you don't setup own secret(s), a secret string will be auto-generated from other config settings."""),
+    ('DesktopEdition',
+     False,
+     "if True, give all local users special powers - ''only use this for a local desktop wiki!''"),
+    ('SecurityPolicy',
+     None,
+     "Class object hook for implementing security restrictions or relaxations"),
+    ('actions_excluded',
+     ['xmlrpc',  # we do not want wiki admins unknowingly offering xmlrpc service
+      'MyPages',  # only works when used with a non-default SecurityPolicy (e.g. autoadmin)
+      'CopyPage',  # has questionable behaviour regarding subpages a user can't read, but can copy
+     ],
+     "Exclude unwanted actions (list of strings)"),
+
+    ('allow_xslt', False,
+     "if True, enables XSLT processing via 4Suite (note that this enables anyone with enough know-how to insert '''arbitrary HTML''' into your wiki, which is why it defaults to `False`)"),
+
+    ('password_checker', DefaultExpression('_default_password_checker'),
+     'checks whether a password is acceptable (default check is length >= 6, at least 4 different chars, no keyboard sequence, not username used somehow (you can switch this off by using `None`)'),
+
+  )),
+  # ==========================================================================
+  'spam_leech_dos': ('Anti-Spam/Leech/DOS',
+  'These settings help limiting ressource usage and avoiding abuse.',
+  (
+    ('hosts_deny', [], "List of denied IPs; if an IP ends with a dot, it denies a whole subnet (class A, B or C)"),
+    ('surge_action_limits',
+     {# allow max. <count> <action> requests per <dt> secs
+        # action: (count, dt)
+        'all': (30, 30), # all requests (except cache/AttachFile action) count for this limit
+        'default': (30, 60), # default limit for actions without a specific limit
+        'show': (30, 60),
+        'recall': (10, 120),
+        'raw': (20, 40),  # some people use this for css
+        'diff': (30, 60),
+        'fullsearch': (10, 120),
+        'edit': (30, 300), # can be lowered after making preview different from edit
+        'rss_rc': (1, 60),
+        # The following actions are often used for images - to avoid pages with lots of images
+        # (like photo galleries) triggering surge protection, we assign rather high limits:
+        'AttachFile': (90, 60),
+        'cache': (600, 30), # cache action is very cheap/efficient
+     },
+     "Surge protection tries to deny clients causing too much load/traffic, see HelpOnConfiguration/SurgeProtection."),
+    ('surge_lockout_time', 3600, "time [s] someone gets locked out when ignoring the warnings"),
+
+    ('textchas', None,
+     "Spam protection setup using site-specific questions/answers, see HelpOnTextChas."),
+    ('textchas_disabled_group', None,
+     "Name of a group of trusted users who do not get asked !TextCha questions."),
+
+    ('antispam_master_url', "http://master.moinmo.in/?action=xmlrpc2",
+     "where antispam security policy fetches spam pattern updates (if it is enabled)"),
+
+    # a regex of HTTP_USER_AGENTS that should be excluded from logging
+    # and receive a FORBIDDEN for anything except viewing a page
+    # list must not contain 'java' because of twikidraw wanting to save drawing uses this useragent
+    ('ua_spiders',
+     ('archiver|cfetch|charlotte|crawler|curl|gigabot|googlebot|heritrix|holmes|htdig|httrack|httpunit|'
+      'intelix|jeeves|larbin|leech|libwww-perl|linkbot|linkmap|linkwalk|litefinder|mercator|'
+      'microsoft.url.control|mirror| mj12bot|msnbot|msrbot|neomo|nutbot|omniexplorer|puf|robot|scooter|seekbot|'
+      'sherlock|slurp|sitecheck|snoopy|spider|teleport|twiceler|voilabot|voyager|webreaper|wget|yeti'),
+     "A regex of HTTP_USER_AGENTs that should be excluded from logging and are not allowed to use actions."),
+
+    ('unzip_single_file_size', 2.0 * 1000 ** 2,
+     "max. size of a single file in the archive which will be extracted [bytes]"),
+    ('unzip_attachments_space', 200.0 * 1000 ** 2,
+     "max. total amount of bytes can be used to unzip files [bytes]"),
+    ('unzip_attachments_count', 101,
+     "max. number of files which are extracted from the zip file"),
+  )),
+  # ==========================================================================
+  'style': ('Style / Theme / UI related',
+  'These settings control how the wiki user interface will look like.',
+  (
+    ('sitename', u'Untitled Wiki',
+     "Short description of your wiki site, displayed below the logo on each page, and used in RSS documents as the channel title [Unicode]"),
+    ('interwikiname', None, "unique and stable InterWiki name (prefix, moniker) of the site [Unicode], or None"),
+    ('logo_string', None, "The wiki logo top of page, HTML is allowed (`<img>` is possible as well) [Unicode]"),
+    ('html_pagetitle', None, "Allows you to set a specific HTML page title (if None, it defaults to the value of `sitename`)"),
+    ('navi_bar', [u'RecentChanges', u'FindPage', u'HelpContents', ],
+     'Most important page names. Users can add more names in their quick links in user preferences. To link to URL, use `u"[[url|link title]]"`, to use a shortened name for long page name, use `u"[[LongLongPageName|title]]"`. [list of Unicode strings]'),
+
+    ('theme_default', 'modern',
+     "the name of the theme that is used by default (see HelpOnThemes)"),
+    ('theme_force', False,
+     "if True, do not allow to change the theme"),
+
+    ('stylesheets', [],
+     "List of tuples (media, csshref) to insert after theme css, before user css, see HelpOnThemes."),
+
+    ('supplementation_page', False,
+     "if True, show a link to the supplementation page in the theme"),
+    ('supplementation_page_name', u'Discussion',
+     "default name of the supplementation (sub)page [unicode]"),
+    ('supplementation_page_template', u'DiscussionTemplate',
+     "default template used for creation of the supplementation page [unicode]"),
+
+    ('interwiki_preferred', [], "In dialogues, show those wikis at the top of the list."),
+    ('sistersites', [], "list of tuples `('WikiName', 'sisterpagelist_fetch_url')`"),
+
+    ('trail_size', 5,
+     "Number of pages in the trail of visited pages"),
+
+    ('page_footer1', '', "Custom HTML markup sent ''before'' the system footer."),
+    ('page_footer2', '', "Custom HTML markup sent ''after'' the system footer."),
+    ('page_header1', '', "Custom HTML markup sent ''before'' the system header / title area but after the body tag."),
+    ('page_header2', '', "Custom HTML markup sent ''after'' the system header / title area (and body tag)."),
+
+    ('changed_time_fmt', '%H:%M', "Time format used on Recent``Changes for page edits within the last 24 hours"),
+    ('date_fmt', '%Y-%m-%d', "System date format, used mostly in Recent``Changes"),
+    ('datetime_fmt', '%Y-%m-%d %H:%M:%S', 'Default format for dates and times (when the user has no preferences or chose the "default" date format)'),
+    ('chart_options', None, "If you have gdchart, use something like chart_options = {'width': 720, 'height': 540}"),
+
+    ('edit_bar', ['Edit', 'Comments', 'Discussion', 'Info', 'Subscribe', 'Quicklink', 'Attachments', 'ActionsMenu'],
+     'list of edit bar entries'),
+    ('history_count', (100, 200), "number of revisions shown for info/history action (default_count_shown, max_count_shown)"),
+
+    ('show_hosts', True,
+     "if True, show host names and IPs. Set to False to hide them."),
+    ('show_interwiki', False,
+     "if True, let the theme display your interwiki name"),
+    ('show_names', True,
+     "if True, show user names in the revision history and on Recent``Changes. Set to False to hide them."),
+    ('show_section_numbers', False,
+     'show section numbers in headings by default'),
+    ('show_timings', False, "show some timing values at bottom of a page"),
+    ('show_version', False, "show moin's version at the bottom of a page"),
+    ('traceback_show', True,
+     "if True, show debug tracebacks to users when moin crashes"),
+
+    ('packagepages_actions_excluded',
+     ['setthemename',  # related to questionable theme stuff, see below
+      'copythemefile', # maybe does not work, e.g. if no fs write permissions or real theme file path is unknown to moin
+      'installplugin', # code installation, potentially dangerous
+      'renamepage',    # dangerous with hierarchical acls
+      'deletepage',    # dangerous with hierarchical acls
+      'delattachment', # dangerous, no revisioning
+     ],
+     'list with excluded package actions (e.g. because they are dangerous / questionable)'),
+
+    ('page_credits',
+     [
+       '<a href="http://moinmo.in/" title="This site uses the MoinMoin Wiki software.">MoinMoin Powered</a>',
+       '<a href="http://moinmo.in/Python" title="MoinMoin is written in Python.">Python Powered</a>',
+       '<a href="http://moinmo.in/GPL" title="MoinMoin is GPL licensed.">GPL licensed</a>',
+       '<a href="http://validator.w3.org/check?uri=referer" title="Click here to validate this page.">Valid HTML 4.01</a>',
+     ],
+     'list with html fragments with logos or strings for crediting.'),
+
+    # These icons will show in this order in the iconbar, unless they
+    # are not relevant, e.g email icon when the wiki is not configured
+    # for email.
+    ('page_iconbar', ["up", "edit", "view", "diff", "info", "subscribe", "raw", "print", ],
+     'list of icons to show in iconbar, valid values are only those in page_icons_table. Available only in classic theme.'),
+
+    # Standard buttons in the iconbar
+    ('page_icons_table',
+     {
+        # key           pagekey, querystr dict, title, icon-key
+        'diff': ('page', {'action': 'diff'}, _("Diffs"), "diff"),
+        'info': ('page', {'action': 'info'}, _("Info"), "info"),
+        'edit': ('page', {'action': 'edit'}, _("Edit"), "edit"),
+        'unsubscribe': ('page', {'action': 'unsubscribe'}, _("UnSubscribe"), "unsubscribe"),
+        'subscribe': ('page', {'action': 'subscribe'}, _("Subscribe"), "subscribe"),
+        'raw': ('page', {'action': 'raw'}, _("Raw"), "raw"),
+        'xml': ('page', {'action': 'show', 'mimetype': 'text/xml'}, _("XML"), "xml"),
+        'print': ('page', {'action': 'print'}, _("Print"), "print"),
+        'view': ('page', {}, _("View"), "view"),
+        'up': ('page_parent_page', {}, _("Up"), "up"),
+     },
+     "dict of {'iconname': (url, title, icon-img-key), ...}. Available only in classic theme."),
+
+  )),
+  # ==========================================================================
+  'editor': ('Editor related', None, (
+    ('editor_default', 'text', "Editor to use by default, 'text' or 'gui'"),
+    ('editor_force', False, "if True, force using the default editor"),
+    ('editor_ui', 'freechoice', "Editor choice shown on the user interface, 'freechoice' or 'theonepreferred'"),
+    ('page_license_enabled', False, 'if True, show a license hint in page editor.'),
+    ('page_license_page', u'WikiLicense', 'Page linked from the license hint. [Unicode]'),
+    ('edit_locking', 'warn 10', "Editor locking policy: `None`, `'warn <timeout in minutes>'`, or `'lock <timeout in minutes>'`"),
+    ('edit_ticketing', True, None),
+    ('edit_rows', 20, "Default height of the edit box"),
+
+  )),
+  # ==========================================================================
+  'paths': ('Paths', None, (
+    ('data_dir', './data/', "Path to the data directory containing your (locally made) wiki pages."),
+    ('data_underlay_dir', './underlay/', "Path to the underlay directory containing distribution system and help pages."),
+    ('cache_dir', None, "Directory for caching, by default computed from `data_dir`/cache."),
+    ('user_dir', None, "Directory for user storage, by default computed to be `data_dir`/user."),
+    ('plugin_dir', None, "Plugin directory, by default computed to be `data_dir`/plugin."),
+    ('plugin_dirs', [], "Additional plugin directories."),
+
+    ('docbook_html_dir', r"/usr/share/xml/docbook/stylesheet/nwalsh/html/",
+     'Path to the directory with the Docbook to HTML XSLT files (optional, used by the docbook parser). The default value is correct for Debian Etch.'),
+    ('shared_intermap', None,
+     "Path to a file containing global InterWiki definitions (or a list of such filenames)"),
+  )),
+  # ==========================================================================
+  'urls': ('URLs', None, (
+    # includes the moin version number, so we can have a unlimited cache lifetime
+    # for the static stuff. if stuff changes on version upgrade, url will change
+    # immediately and we have no problem with stale caches.
+    ('url_prefix_static', config.url_prefix_static,
+     "used as the base URL for icons, css, etc. - includes the moin version number and changes on every release. This replaces the deprecated and sometimes confusing `url_prefix = '/wiki'` setting."),
+    ('url_prefix_local', None,
+     "used as the base URL for some Javascript - set this to a URL on same server as the wiki if your url_prefix_static points to a different server."),
+    ('url_prefix_fckeditor', None,
+     "used as the base URL for FCKeditor - similar to url_prefix_local, but just for FCKeditor."),
+
+    ('url_prefix_action', None,
+     "Use 'action' to enable action URL generation to be compatible with robots.txt. It will generate .../action/info/PageName?action=info then. Recommended for internet wikis."),
+
+    ('notification_bot_uri', None, "URI of the Jabber notification bot."),
+
+    ('url_mappings', {},
+     "lookup table to remap URL prefixes (dict of {{{'prefix': 'replacement'}}}); especially useful in intranets, when whole trees of externally hosted documents move around"),
+
+  )),
+  # ==========================================================================
+  'pages': ('Special page names', None, (
+    ('page_front_page', u'HelpOnLanguages',
+     "Name of the front page. We don't expect you to keep the default. Just read HelpOnLanguages in case you're wondering... [Unicode]"),
+
+    # the following regexes should match the complete name when used in free text
+    # the group 'all' shall match all, while the group 'key' shall match the key only
+    # e.g. CategoryFoo -> group 'all' ==  CategoryFoo, group 'key' == Foo
+    # moin's code will add ^ / $ at beginning / end when needed
+    ('page_category_regex', ur'(?P<all>Category(?P<key>(?!Template)\S+))',
+     'Pagenames exactly matching this regex are regarded as Wiki categories [Unicode]'),
+    ('page_dict_regex', ur'(?P<all>(?P<key>\S+)Dict)',
+     'Pagenames exactly matching this regex are regarded as pages containing variable dictionary definitions [Unicode]'),
+    ('page_group_regex', ur'(?P<all>(?P<key>\S+)Group)',
+     'Pagenames exactly matching this regex are regarded as pages containing group definitions [Unicode]'),
+    ('page_template_regex', ur'(?P<all>(?P<key>\S+)Template)',
+     'Pagenames exactly matching this regex are regarded as pages containing templates for new pages [Unicode]'),
+
+    ('page_local_spelling_words', u'LocalSpellingWords',
+     'Name of the page containing user-provided spellchecker words [Unicode]'),
+  )),
+  # ==========================================================================
+  'user': ('User Preferences related', None, (
+    ('quicklinks_default', [],
+     'List of preset quicklinks for a newly created user accounts. Existing accounts are not affected by this option whereas changes in navi_bar do always affect existing accounts. Preset quicklinks can be removed by the user in the user preferences menu, navi_bar settings not.'),
+    ('subscribed_pages_default', [],
+     "List of pagenames used for presetting page subscriptions for newly created user accounts."),
+
+    ('email_subscribed_events_default',
+     [
+        PageChangedEvent.__name__,
+        PageRenamedEvent.__name__,
+        PageDeletedEvent.__name__,
+        PageCopiedEvent.__name__,
+        PageRevertedEvent.__name__,
+        FileAttachedEvent.__name__,
+     ], None),
+    ('jabber_subscribed_events_default', [], None),
+
+    ('tz_offset', 0.0,
+     "default time zone offset in hours from UTC"),
+
+    ('userprefs_disabled', [],
+     "Disable the listed user preferences plugins."),
+  )),
+  # ==========================================================================
+  'various': ('Various', None, (
+    ('bang_meta', True, 'if True, enable {{{!NoWikiName}}} markup'),
+    ('caching_formats', ['text_html'], "output formats that are cached; set to [] to turn off caching (useful for development)"),
+
+    ('config_check_enabled', False, "if True, check configuration for unknown settings."),
+
+    ('default_markup', 'wiki', 'Default page parser / format (name of module in `MoinMoin.parser`)'),
+
+    ('html_head', '', "Additional <HEAD> tags, see HelpOnThemes."),
+    ('html_head_queries', '<meta name="robots" content="noindex,nofollow">\n',
+     "Additional <HEAD> tags for requests with query strings, like actions."),
+    ('html_head_posts', '<meta name="robots" content="noindex,nofollow">\n',
+     "Additional <HEAD> tags for POST requests."),
+    ('html_head_index', '<meta name="robots" content="index,follow">\n',
+     "Additional <HEAD> tags for some few index pages."),
+    ('html_head_normal', '<meta name="robots" content="index,nofollow">\n',
+     "Additional <HEAD> tags for most normal pages."),
+
+    ('language_default', 'en', "Default language for user interface and page content, see HelpOnLanguages."),
+    ('language_ignore_browser', False, "if True, ignore user's browser language settings, see HelpOnLanguages."),
+
+    ('log_remote_addr', True,
+     "if True, log the remote IP address (and maybe hostname)."),
+    ('log_reverse_dns_lookups', True,
+     "if True, do a reverse DNS lookup on page SAVE. If your DNS is broken, set this to False to speed up SAVE."),
+    ('log_timing', False,
+     "if True, add timing infos to the log output to analyse load conditions"),
+
+    # some dangerous mimetypes (we don't use "content-disposition: inline" for them when a user
+    # downloads such attachments, because the browser might execute e.g. Javascript contained
+    # in the HTML and steal your moin session cookie or do other nasty stuff)
+    ('mimetypes_xss_protect',
+     [
+       'text/html',
+       'application/x-shockwave-flash',
+       'application/xhtml+xml',
+     ],
+     '"content-disposition: inline" isn\'t used for them when a user downloads such attachments'),
+
+    ('mimetypes_embed',
+     [
+       'application/x-dvi',
+       'application/postscript',
+       'application/pdf',
+       'application/ogg',
+       'application/vnd.visio',
+       'image/x-ms-bmp',
+       'image/svg+xml',
+       'image/tiff',
+       'image/x-photoshop',
+       'audio/mpeg',
+       'audio/midi',
+       'audio/x-wav',
+       'video/fli',
+       'video/mpeg',
+       'video/quicktime',
+       'video/x-msvideo',
+       'chemical/x-pdb',
+       'x-world/x-vrml',
+     ],
+     'mimetypes that can be embedded by the [[HelpOnMacros/EmbedObject|EmbedObject macro]]'),
+
+    ('refresh', None,
+     "refresh = (minimum_delay_s, targets_allowed) enables use of `#refresh 5 PageName` processing instruction, targets_allowed must be either `'internal'` or `'external'`"),
+    ('rss_cache', 60, "suggested caching time for Recent''''''Changes RSS, in second"),
+
+    ('search_results_per_page', 25, "Number of hits shown per page in the search results"),
+
+    ('siteid', 'default', None),
+  )),
+}
+
+#
+# The 'options' dict carries default MoinMoin options. The dict is a
+# group name to tuple mapping.
+# Each group tuple consists of the following items:
+#   group section heading, group help text, option list
+#
+# where each 'option list' is a tuple or list of option tuples
+#
+# each option tuple consists of
+#   option name, default value, help text
+#
+# All the help texts will be displayed by the WikiConfigHelp() macro.
+#
+# Unlike the options_no_group_name dict, option names in this dict
+# are automatically prefixed with "group name '_'" (i.e. the name of
+# the group they are in and an underscore), e.g. the 'hierarchic'
+# below creates an option called "acl_hierarchic".
+#
+# If you need to add a complex default expression that results in an
+# object and should not be shown in the __repr__ form in WikiConfigHelp(),
+# you can use the DefaultExpression class, see 'auth' above for example.
+#
+#
+options = {
+    'acl': ('Access control lists',
+    'ACLs control who may do what, see HelpOnAccessControlLists.',
+    (
+      ('hierarchic', False, 'True to use hierarchical ACLs'),
+      ('rights_default', u"Trusted:read,write,delete,revert Known:read,write,delete,revert All:read,write",
+       "ACL used if no ACL is specified on the page"),
+      ('rights_before', u"",
+       "ACL that is processed before the on-page/default ACL"),
+      ('rights_after', u"",
+       "ACL that is processed after the on-page/default ACL"),
+      ('rights_valid', ['read', 'write', 'delete', 'revert', 'admin'],
+       "Valid tokens for right sides of ACL entries."),
+    )),
+
+    'xapian': ('Xapian search', "Configuration of the Xapian based indexed search, see HelpOnXapian.", (
+      ('search', False,
+       "True to enable the fast, indexed search (based on the Xapian search library)"),
+      ('index_dir', None,
+       "Directory where the Xapian search index is stored (None = auto-configure wiki local storage)"),
+      ('stemming', False,
+       "True to enable Xapian word stemmer usage for indexing / searching."),
+      ('index_history', False,
+       "True to enable indexing of non-current page revisions."),
+    )),
+
+    'user': ('Users / User settings', None, (
+      ('email_unique', True,
+       "if True, check email addresses for uniqueness and don't accept duplicates."),
+      ('jid_unique', True,
+       "if True, check Jabber IDs for uniqueness and don't accept duplicates."),
+
+      ('homewiki', u'Self',
+       "interwiki name of the wiki where the user home pages are located [Unicode] - useful if you have ''many'' users. You could even link to nonwiki \"user pages\" if the wiki username is in the target URL."),
+
+      ('checkbox_fields',
+       [
+        ('mailto_author', lambda _: _('Publish my email (not my wiki homepage) in author info')),
+        ('edit_on_doubleclick', lambda _: _('Open editor on double click')),
+        ('remember_last_visit', lambda _: _('After login, jump to last visited page')),
+        ('show_comments', lambda _: _('Show comment sections')),
+        ('show_nonexist_qm', lambda _: _('Show question mark for non-existing pagelinks')),
+        ('show_page_trail', lambda _: _('Show page trail')),
+        ('show_toolbar', lambda _: _('Show icon toolbar')),
+        ('show_topbottom', lambda _: _('Show top/bottom links in headings')),
+        ('show_fancy_diff', lambda _: _('Show fancy diffs')),
+        ('wikiname_add_spaces', lambda _: _('Add spaces to displayed wiki names')),
+        ('remember_me', lambda _: _('Remember login information')),
+
+        ('disabled', lambda _: _('Disable this account forever')),
+        # if an account is disabled, it may be used for looking up
+        # id -> username for page info and recent changes, but it
+        # is not usable for the user any more:
+       ],
+       "Describes user preferences, see HelpOnConfiguration/UserPreferences."),
+
+      ('checkbox_defaults',
+       {
+        'mailto_author': 0,
+        'edit_on_doubleclick': 0,
+        'remember_last_visit': 0,
+        'show_comments': 0,
+        'show_nonexist_qm': False,
+        'show_page_trail': 1,
+        'show_toolbar': 1,
+        'show_topbottom': 0,
+        'show_fancy_diff': 1,
+        'wikiname_add_spaces': 0,
+        'remember_me': 1,
+       },
+       "Defaults for user preferences, see HelpOnConfiguration/UserPreferences."),
+
+      ('checkbox_disable', [],
+       "Disable user preferences, see HelpOnConfiguration/UserPreferences."),
+
+      ('checkbox_remove', [],
+       "Remove user preferences, see HelpOnConfiguration/UserPreferences."),
+
+      ('form_fields',
+       [
+        ('name', _('Name'), "text", "36", _("(Use FirstnameLastname)")),
+        ('aliasname', _('Alias-Name'), "text", "36", ''),
+        ('email', _('Email'), "text", "36", ''),
+        ('jid', _('Jabber ID'), "text", "36", ''),
+        ('css_url', _('User CSS URL'), "text", "40", _('(Leave it empty for disabling user CSS)')),
+        ('edit_rows', _('Editor size'), "text", "3", ''),
+       ],
+       None),
+
+      ('form_defaults',
+       {# key: default - do NOT remove keys from here!
+        'name': '',
+        'aliasname': '',
+        'password': '',
+        'password2': '',
+        'email': '',
+        'jid': '',
+        'css_url': '',
+        'edit_rows': "20",
+       },
+       None),
+
+      ('form_disable', [], "list of field names used to disable user preferences form fields"),
+
+      ('form_remove', [], "list of field names used to remove user preferences form fields"),
+
+      ('transient_fields',
+       ['id', 'valid', 'may', 'auth_username', 'password', 'password2', 'auth_method', 'auth_attribs', ],
+       "User object attributes that are not persisted to permanent storage (internal use)."),
+    )),
+
+    'openid_server': ('OpenID Server',
+        'These settings control the built-in OpenID Identity Provider (server).',
+    (
+      ('enabled', False, "True to enable the built-in OpenID server."),
+      ('restricted_users_group', None, "If set to a group name, the group members are allowed to use the wiki as an OpenID provider. (None = allow for all users)"),
+      ('enable_user', False, "If True, the OpenIDUser processing instruction is allowed."),
+    )),
+
+    'mail': ('Mail settings',
+        'These settings control outgoing and incoming email from and to the wiki.',
+    (
+      ('from', None, "Used as From: address for generated mail."),
+      ('login', None, "'username userpass' for SMTP server authentication (None = don't use auth)."),
+      ('smarthost', None, "Address of SMTP server to use for sending mail (None = don't use SMTP server)."),
+      ('sendmail', None, "sendmail command to use for sending mail (None = don't use sendmail)"),
+
+      ('import_subpage_template', u"$from-$date-$subject", "Create subpages using this template when importing mail."),
+      ('import_pagename_search', ['subject', 'to', ], "Where to look for target pagename specification."),
+      ('import_pagename_envelope', u"%s", "Use this to add some fixed prefix/postfix to the generated target pagename."),
+      ('import_pagename_regex', r'\[\[([^\]]*)\]\]', "Regular expression used to search for target pagename specification."),
+      ('import_wiki_addrs', [], "Target mail addresses to consider when importing mail"),
+    )),
+
+    'backup': ('Backup settings',
+        'These settings control how the backup action works and who is allowed to use it.',
+    (
+      ('compression', 'gz', 'What compression to use for the backup ("gz" or "bz2").'),
+      ('users', [], 'List of trusted user names who are allowed to get a backup.'),
+      ('include', [], 'List of pathes to backup.'),
+      ('exclude', lambda self, filename: False, 'Function f(self, filename) that tells whether a file should be excluded from backup. By default, nothing is excluded.'),
+    )),
+}
+
+def _add_options_to_defconfig(opts, addgroup=True):
+    for groupname in opts:
+        group_short, group_doc, group_opts = opts[groupname]
+        for name, default, doc in group_opts:
+            if addgroup:
+                name = groupname + '_' + name
+            if isinstance(default, DefaultExpression):
+                default = default.value
+            setattr(DefaultConfig, name, default)
+
+_add_options_to_defconfig(options)
+_add_options_to_defconfig(options_no_group_name, False)
+
 # remove the gettext pseudo function
 del _
 
--- a/MoinMoin/conftest.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/conftest.py	Wed Feb 03 13:37:38 2010 +0100
@@ -12,34 +12,28 @@
 classes by the framework.
 
 Tests that require a certain configuration, like section_numbers = 1, must
-use a TestConfig to create the required configuration before the test.
-Deleting the TestConfig instance will restore the previous configuration.
+use a Config class to define the required configuration within the test class.
 
-@copyright: 2005 Nir Soffer, 2007 Alexander Schremmer
+@copyright: 2005 MoinMoin:NirSoffer,
+            2007 MoinMoin:AlexanderSchremmer,
+            2008 MoinMoin:ThomasWaldmann
 @license: GNU GPL, see COPYING for details.
 """
 
 import atexit
-from sys import modules
 import sys
 
 import py
 
-
 rootdir = py.magic.autopath().dirpath()
 moindir = rootdir.join("..")
-
 sys.path.insert(0, str(moindir))
-from MoinMoin._tests import maketestwiki, compat
-modules["unittest"] = compat # evil hack
-
-wikiconfig_dir = str(moindir.join("tests"))
 
 from MoinMoin.support.python_compatibility import set
+from MoinMoin._tests import maketestwiki, wikiconfig
 
 coverage_modules = set()
 
-
 try:
     """
     This code adds support for coverage.py (see
@@ -52,7 +46,7 @@
 
     def report_coverage():
         coverage.stop()
-        module_list = [modules[mod] for mod in coverage_modules]
+        module_list = [sys.modules[mod] for mod in coverage_modules]
         module_list.sort()
         coverage.report(module_list)
 
@@ -70,17 +64,14 @@
     coverage = None
 
 
-def init_test_request(static_state=[False]):
+def init_test_request(given_config=None, static_state=[False]):
     from MoinMoin.request import request_cli
     from MoinMoin.user import User
     from MoinMoin.formatter.text_html import Formatter as HtmlFormatter
     if not static_state[0]:
         maketestwiki.run(True)
         static_state[0] = True
-    if sys.path[0] != wikiconfig_dir:
-        sys.path.insert(0, wikiconfig_dir) # this is a race with py.test's collectors
-                                           # because they modify sys.path as well
-    request = request_cli.Request()
+    request = request_cli.Request(given_config=given_config)
     request.form = request.args = request.setup_args()
     request.user = User(request)
     request.html_formatter = HtmlFormatter(request)
@@ -88,85 +79,14 @@
     return request
 
 
-class TestConfig:
-    """ Custom configuration for unit tests
-
-    Some tests assume a specific configuration, and will fail if the wiki admin
-    changed the configuration. For example, DateTime macro test assume
-    the default datetime_fmt.
-
-    When you set custom values in a TestConfig, the previous values are saved,
-    and when the TestConfig is called specifically, they are restored automatically.
-
-    Typical Usage
-    -------------
-    ::
-        class SomeTest:
-            def setUp(self):
-                self.config = self.TestConfig(defaults=key_list, key=value,...)
-            def tearDown(self):
-                self.config.restore()
-            def testSomething(self):
-                # test that needs those defaults and custom values
-    """
-
-    def __init__(self, request):
-        """ Create temporary configuration for a test
-
-        @param request: current request
-        """
-        self.request = request
-        self.old = {}  # Old config values
-        self.new = []  # New added attributes
-
-    def __call__(self, defaults=(), **custom):
-        """ Initialise a temporary configuration for a test
+# py.test customization starts here
 
-        @param defaults: list of keys that should use the default value
-        @param custom: other keys using non default values, or new keys
-               that request.cfg does not have already
-        """
-        self.setDefaults(defaults)
-        self.setCustom(**custom)
-
-        return self
-
-    def setDefaults(self, defaults=()):
-        """ Set default values for keys in defaults list
-
-        Non existing default will raise an AttributeError.
-        """
-        from MoinMoin.config import multiconfig
-        for key in defaults:
-            self._setattr(key, getattr(multiconfig.DefaultConfig, key))
-
-    def setCustom(self, **custom):
-        """ Set custom values """
-        for key, value in custom.items():
-            self._setattr(key, value)
-
-    def _setattr(self, key, value):
-        """ Set a new value for key saving new added keys """
-        if hasattr(self.request.cfg, key):
-            self.old[key] = getattr(self.request.cfg, key)
-        else:
-            self.new.append(key)
-        setattr(self.request.cfg, key, value)
-
-    def restore(self):
-        """ Restore previous request.cfg
-
-        Set old keys to old values and delete new keys.
-        """
-        for key, value in self.old.items():
-            setattr(self.request.cfg, key, value)
-        for key in self.new:
-            delattr(self.request.cfg, key)
-    __del__ = restore # XXX __del__ semantics are currently broken
-
-
-
-# py.test customization starts here
+# py.test-1.0 provides "funcargs" natively
+def pytest_funcarg__request(request):
+    # note the naminng clash: py.test's funcarg-request object
+    # and the request we provide are totally separate things
+    cls = request._pyfuncitem.getparent(py.test.collect.Module)
+    return cls.request
 
 class MoinTestFunction(py.test.collect.Function):
     def execute(self, target, *args):
@@ -183,8 +103,10 @@
 
     def setup(self):
         cls = self.obj
-        cls.request = self.parent.request
-        cls.TestConfig = TestConfig(cls.request)
+        if hasattr(cls, 'Config'):
+            cls.request = init_test_request(given_config=cls.Config)
+        else:
+            cls.request = self.parent.request
         super(MoinClassCollector, self).setup()
 
 
@@ -193,7 +115,7 @@
     Function = MoinTestFunction
 
     def __init__(self, *args, **kwargs):
-        self.request = init_test_request()
+        self.request = init_test_request(given_config=wikiconfig.Config)
         super(Module, self).__init__(*args, **kwargs)
 
     def run(self, *args, **kwargs):
--- a/MoinMoin/converter/_tests/test_text_html_text_moin_wiki.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/converter/_tests/test_text_html_text_moin_wiki.py	Wed Feb 03 13:37:38 2010 +0100
@@ -23,12 +23,6 @@
 
 class TestBase(object):
 
-    def setup_method(self, method):
-        self.cfg = self.TestConfig(bang_meta=True)
-
-    def teardown_method(self, method):
-        del self.cfg
-
     def do_convert_real(self, func_args, successful=True):
         try:
             ret = convert(*func_args)
@@ -788,6 +782,67 @@
 """
         self.do(test, output)
 
+    def testPreSuccess10(self):
+        test = ur"""
+ * {{{{
+{{{
+test
+}}}
+}}}}
+
+"""
+        output = ur"""
+<ul>
+<li>
+<pre>
+{{{
+test
+}}}
+</pre>
+</li>
+</ul>
+"""
+
+    def testPreSuccess11(self):
+        test = ur"""
+ * {{{{
+test
+}}}
+}}}}
+
+"""
+        output = ur"""
+<ul>
+<li>
+<pre>
+test
+}}}
+</pre>
+</li>
+</ul>
+"""
+
+    def testPreSuccess12(self):
+        test = ur"""
+ * {{{{
+{{{
+test
+}}}}
+
+"""
+        output = ur"""
+<ul>
+<li>
+<pre>
+{{{
+test
+</pre>
+</li>
+</ul>
+"""
+
+        self.do(test, output)
+
     def testRule1(self):
         py.test.skip('broken test')
         test = ur"""
--- a/MoinMoin/converter/text_html_text_moin_wiki.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/converter/text_html_text_moin_wiki.py	Wed Feb 03 13:37:38 2010 +0100
@@ -13,6 +13,9 @@
 from MoinMoin import config, wikiutil
 from MoinMoin.error import ConvertError
 
+from MoinMoin.parser.text_moin_wiki import Parser as WikiParser
+interwiki_re = re.compile(WikiParser.interwiki_rule, re.VERBOSE|re.UNICODE)
+
 # Portions (C) International Organization for Standardization 1986
 # Permission to copy in any form is granted for use with
 # conforming SGML systems and applications as defined in
@@ -571,7 +574,7 @@
     def process_heading(self, node):
         text = self.node_list_text_only(node.childNodes).strip()
         if text:
-            depth = int(node.localName[1]) - 1
+            depth = int(node.localName[1])
             hstr = "=" * depth
             self.text.append(self.new_line)
             self.text.append("%s %s %s" % (hstr, text.replace("\n", " "), hstr))
@@ -606,6 +609,11 @@
             style = listitem.getAttribute("style")
             if re.match(ur"list-style-type:\s*none", style, re.I):
                 markup = ". "
+                # set markup with white space when list element containes table
+                for i in listitem.childNodes:
+                    if i.nodeType == Node.ELEMENT_NODE:
+                        if i.localName == 'table':
+                            markup = ""
             else:
                 markup = "* "
         elif name == 'dl':
@@ -661,7 +669,10 @@
             self.text.append(indent)
         for i in nodelist:
             if i.nodeType == Node.ELEMENT_NODE:
-                self.process_inline(i)
+                if i.localName == 'br':
+                    self.text.append('<<BR>>')
+                else:
+                    self.process_inline(i)
             elif i.nodeType == Node.TEXT_NODE:
                 self.text.append(i.data.strip('\n').replace('\n', ' '))
         self.text.append(self.new_line)
@@ -671,6 +682,12 @@
         found = False
         need_indent = False
         pending = []
+
+        # If this is a empty list item, we just terminate the line
+        if node.childNodes.length == 0:
+            self.text.append(self.new_line)
+            return
+
         for i in node.childNodes:
             name = i.localName
 
@@ -700,6 +717,8 @@
                     self.text.append(indent)
                 self.process_table(i)
                 found = True
+            elif name == 'br':
+                pending.append(i)
             else:
                 pending.append(i)
 
@@ -792,7 +811,8 @@
             command = ",,"
         elif name == 'sup':
             command = "^"
-        elif name in ('font', 'meta', ):
+        elif name in ('area', 'center', 'code', 'embed', 'fieldset', 'font', 'form', 'iframe', 'input', 'label', 'link', 'map',
+                      'meta', 'noscript', 'option', 'script', 'select', 'textarea', 'wbr'):
             command = "" # just throw away unsupported elements
         else:
             raise ConvertError("process_inline: Don't support %s element" % name)
@@ -815,16 +835,48 @@
         self.text.append(command)
 
     def process_span(self, node):
-        # ignore span tags - just descend
+        # process span tag for firefox3
+        node_style = node.getAttribute("style")
+
         is_strike = node.getAttribute("class") == "strike"
+        is_strike = is_strike or "line-through" in node_style
+        is_strong = "bold" in node_style
+        is_italic = "italic" in node_style
+        is_underline = "underline" in node_style
+        is_comment = node.getAttribute("class") == "comment"
+
+        # start tag
+        if is_comment:
+            self.text.append("/* ")
         if is_strike:
             self.text.append("--(")
+        if is_strong:
+            self.text.append("'''")
+        if is_italic:
+            self.text.append("''")
+        if is_underline:
+            self.text.append("__")
+
+        # body
         for i in node.childNodes:
             self.process_inline(i)
+
+        # end tag
+        if is_underline:
+            self.text.append("__")
+        if is_italic:
+            self.text.append("''")
+        if is_strong:
+            self.text.append("'''")
         if is_strike:
             self.text.append(")--")
+        if is_comment:
+            self.text.append(" */")
 
     def process_div(self, node):
+        # process indent
+        self._process_indent(node)
+
         # ignore div tags - just descend
         for i in node.childNodes:
             self.visit(i)
@@ -848,9 +900,21 @@
         self.text.extend([self.new_line, "-" * length, self.new_line])
 
     def process_p(self, node):
+        # process indent
+        self._process_indent(node)
         self.process_paragraph_item(node)
         self.text.append("\n\n") # do not use self.new_line here!
 
+    def _process_indent(self, node):
+        # process indent
+        node_style = node.getAttribute("style")
+        match = re.match(r"margin-left:\s*(\d+)px", node_style)
+        if match:
+            left_margin = int(match.group(1))
+            indent_depth = int(left_margin / 40)
+            if indent_depth > 0:
+                self.text.append(' . ')
+
     def process_paragraph_item(self, node):
         for i in node.childNodes:
             if i.nodeType == Node.ELEMENT_NODE:
@@ -870,25 +934,59 @@
         if class_ == "comment": # we currently use this for stuff like ## or #acl
             for i in node.childNodes:
                 if i.nodeType == Node.TEXT_NODE:
-                    self.text.append(i.data)
-                    #print "'%s'" % i.data
+                    self.text.append(i.data.replace('\n', ''))
                 elif i.localName == 'br':
                     self.text.append(self.new_line)
                 else:
                     pass
-                    #print i.localName
         else:
-            self.text.extend(["{{{", self.new_line])
+            content_buffer = []
+            longest_inner_formater = ''
+            bang_args = ''
+            delimiters = []
+
+            """
+            below code fixed for MoinMoinBugs/GuiEditorCantNest bug
+            this has problem when outer delimiter has two more { than inside one
+            e.g. {{{{{{ {{{ foo }}} }}}}}}  --> {{{{ {{{ foo }}} }}}}
+                   {{{foo {{{ }}} foo}}} --> {{{{ {{{ }}} }}}}
+            """
+
             for i in node.childNodes:
                 if i.nodeType == Node.TEXT_NODE:
-                    self.text.append(i.data)
-                    #print "'%s'" % i.data
+                    # get longest pre tag({{{ or }}}) from content
+                    delimiters.extend(re.compile("((?u){+)").findall(i.data))
+                    delimiters.extend(re.compile("((?u)}+)").findall(i.data))
+                    # when first line is empty, start iteration second line of i.data
+                    data_lines = i.data.rstrip().split('\n')
+                    if data_lines[0].strip() == '':
+                        data_lines = data_lines[1:]
+                    for line in data_lines:
+                        if line.strip().startswith('#!'):
+                            if bang_args == '':
+                                bang_args = line.strip()
+                            else:
+                                content_buffer.extend([line, self.new_line])
+                        else:
+                            content_buffer.extend([line, self.new_line])
                 elif i.localName == 'br':
-                    self.text.append(self.new_line_dont_remove)
+                    content_buffer.append(self.new_line_dont_remove)
                 else:
                     pass
-                    #print i.localName
-            self.text.extend(["}}}", self.new_line])
+
+            if delimiters:
+                longest_inner_formater = max(delimiters)
+
+            if (len(longest_inner_formater) >= 3):
+                self.text.extend([("{" * (len(longest_inner_formater) + 1)) + bang_args, \
+                                      self.new_line])
+                self.text.extend(content_buffer)
+                self.text.extend(["}" * (len(longest_inner_formater) + 1), \
+                                      self.new_line])
+            else:
+                self.text.extend(["{{{"+bang_args, self.new_line])
+                self.text.extend(content_buffer)
+                self.text.extend(["}}}", self.new_line])
 
     _alignment = {"left": "(",
                   "center": ":",
@@ -903,19 +1001,26 @@
         except ValueError:
             return value
 
-    def _table_style(self, node):
-        # TODO: attrs = get_attrs(node)
-        result = []
+    def _get_color(self, node, prefix):
         if node.hasAttribute("bgcolor"):
             value = node.getAttribute("bgcolor")
             match = re.match(r"rgb\((\d+),\s*(\d+),\s*(\d+)\)", value)
             if match:
-                result.append('tablebgcolor="#%X%X%X"' %
-                              (int(match.group(1)),
-                               int(match.group(2)),
-                               int(match.group(3))))
+                value = '#%X%X%X' % (int(match.group(1)), int(match.group(2)), int(match.group(3)))
             else:
-                result.append('tablebgcolor="%s"' % value)
+                match = re.match(r"#[0-9A-Fa-f]{6}", value)
+            if not prefix and match:
+                result = value
+            else:
+                result = '%sbgcolor="%s"' % (prefix, value)
+        else:
+            result = ''
+        return result
+
+    def _table_style(self, node):
+        # TODO: attrs = get_attrs(node)
+        result = []
+        result.append(self._get_color(node, 'table'))
         if node.hasAttribute("width"):
             value = node.getAttribute("width")
             result.append('tablewidth="%s"' % self._check_length(value))
@@ -934,16 +1039,7 @@
     def _row_style(self, node):
         # TODO: attrs = get_attrs(node)
         result = []
-        if node.hasAttribute("bgcolor"):
-            value = node.getAttribute("bgcolor")
-            match = re.match(r"rgb\((\d+),\s*(\d+),\s*(\d+)\)", value)
-            if match:
-                result.append('rowbgcolor="#%X%X%X"' %
-                              (int(match.group(1)),
-                               int(match.group(2)),
-                               int(match.group(3))))
-            else:
-                result.append('rowbgcolor="%s"' % value)
+        result.append(self._get_color(node, 'row'))
         if node.hasAttribute("style"):
             result.append('rowstyle="%s"' % node.getAttribute("style"))
         if node.hasAttribute("class"):
@@ -966,15 +1062,7 @@
 
         align = ""
         result = []
-        if  node.hasAttribute("bgcolor"):
-            value = node.getAttribute("bgcolor")
-            match = re.match(r"rgb\((\d+),\s*(\d+),\s*(\d+)\)", value)
-            if match:
-                result.append("#%X%X%X" % (int(match.group(1)),
-                                           int(match.group(2)),
-                                           int(match.group(3))))
-            else:
-                result.append('bgcolor="%s"' % value)
+        result.append(self._get_color(node, ''))
         if node.hasAttribute("align"):
             value = node.getAttribute("align")
             if not spanning or value != "center":
@@ -1007,7 +1095,8 @@
         return " ".join(result).strip()
 
     def process_table(self, node, style=""):
-        self.text.append(self.new_line)
+        if self.depth == 0:
+            self.text.append(self.new_line)
         self.new_table = True
         style += self._table_style(node)
         for i in node.childNodes:
@@ -1055,7 +1144,9 @@
             colspan = 1
         text = self.node_list_text_only(node.childNodes).replace('\n', ' ').strip()
         if text:
-            self.text.extend(["%s'''%s%s'''||" % ('||' * colspan, style, text), self.new_line_dont_remove])
+            if style:
+                style = '<%s>' % style
+            self.text.extend(["%s%s'''%s'''||" % ('||' * colspan, style, text), self.new_line_dont_remove])
 
     def process_table_data(self, node, style=""):
         if node.hasAttribute("colspan"):
@@ -1106,7 +1197,7 @@
         for i in node.childNodes:
             if i.nodeType == Node.ELEMENT_NODE:
                 name = i.localName
-                if name == 'td':
+                if name in ('td', 'th', ):
                     self.process_table_data(i, style=style)
                     style = ""
                 else:
@@ -1147,10 +1238,11 @@
                 pagename = wikiutil.url_unquote(href)
                 interwikiname = "%s:%s" % (title, pagename)
             if interwikiname and pagename == desc:
-                if ' ' in interwikiname:
+                if interwiki_re.match(interwikiname+' '): # the blank is needed by interwiki_re to match
+                    # this is valid as a free interwiki link
+                    self.text.append("%s" % interwikiname)
+                else:
                     self.text.append("[[%s]]" % interwikiname)
-                else:
-                    self.text.append("%s" % interwikiname)
                 return
             elif title == 'Self':
                 self.text.append('[[%s|%s]]' % (href, desc))
@@ -1168,10 +1260,17 @@
             # Attachments
             if title.startswith("attachment:"):
                 attname = wikiutil.url_unquote(title[len("attachment:"):])
+                if 'do=get' in href: # quick&dirty fix for not dropping &do=get param
+                    parms = '|&do=get'
+                else:
+                    parms = ''
                 if attname != desc:
-                    self.text.append('[[attachment:%s|%s]]' % (attname, desc))
+                    desc = '|%s' % desc
+                elif parms:
+                    desc = '|'
                 else:
-                    self.text.append('[[attachment:%s]]' % (attname, ))
+                    desc = ''
+                self.text.append('[[attachment:%s%s%s]]' % (attname, desc, parms))
             # wiki link
             elif href.startswith(scriptname):
                 pagename = href[len(scriptname):]
@@ -1187,6 +1286,9 @@
                 # relative link ../
                 elif desc.startswith('../') and href.endswith(desc[3:]):
                     self.text.append(wikiutil.pagelinkmarkup(desc))
+                # internal link #internal
+                elif '#' in href and pagename.startswith(self.pagename):
+                    self.text.append(wikiutil.pagelinkmarkup(href[href.index('#'):], desc))
                 # labeled link
                 else:
                     self.text.append(wikiutil.pagelinkmarkup(pagename, desc))
@@ -1287,7 +1389,16 @@
     """ get the attributes of <node> into an easy-to-use dict """
     attrs = {}
     for attr_name in node.attributes.keys():
-        attrs[attr_name] = node.attributes.get(attr_name).nodeValue
+        # get attributes of style element
+        if attr_name == "style":
+            for style_element in node.attributes.get(attr_name).nodeValue.split(';'):
+                if style_element.strip() != '':
+                    style_elements = style_element.split(':')
+                    if len(style_elements) == 2:
+                        attrs[style_elements[0].strip()] = style_elements[1].strip()
+        # get attributes without style element
+        else:
+            attrs[attr_name] = node.attributes.get(attr_name).nodeValue
     return attrs
 
 
--- a/MoinMoin/events/_tests/test_events.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/events/_tests/test_events.py	Wed Feb 03 13:37:38 2010 +0100
@@ -59,7 +59,7 @@
 
     event = events.UserCreatedEvent(request, User(request))
     request.cfg.notification_server = server_dummy()
-    request.cfg.secret = "dummy"
+    request.cfg.secrets = "thisisnotsecret"
 
     jabbernotify.handle_user_created(event)
     assert request.cfg.notification_server.sent is True
--- a/MoinMoin/events/emailnotify.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/events/emailnotify.py	Wed Feb 03 13:37:38 2010 +0100
@@ -51,7 +51,12 @@
             'username': page.uid_override or user.getUserIdentification(request),
         }
 
-    return {'subject': subject, 'text': change['text'] + pagelink + change['diff']}
+    if change.has_key('comment'):
+        comment = _("Comment:") + "\n" + change['comment'] + "\n\n"
+    else:
+        comment = ''
+
+    return {'subject': subject, 'text': change['text'] + pagelink + comment + change['diff']}
 
 
 def send_notification(request, from_address, emails, data):
--- a/MoinMoin/events/jabbernotify.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/events/jabbernotify.py	Wed Feb 03 13:37:38 2010 +0100
@@ -50,11 +50,12 @@
 
     request = event.request
     server = request.cfg.notification_server
+    secret = request.cfg.secrets['jabberbot']
     try:
         if isinstance(event, ev.JabberIDSetEvent):
-            server.addJIDToRoster(request.cfg.secret, event.jid)
+            server.addJIDToRoster(secret, event.jid)
         else:
-            server.removeJIDFromRoster(request.cfg.secret, event.jid)
+            server.removeJIDFromRoster(secret, event.jid)
     except xmlrpclib.Error, err:
         logging.error("XML RPC error: %s" % str(err))
     except Exception, err:
@@ -197,7 +198,7 @@
         raise ValueError("url_list must be of type list!")
 
     try:
-        server.send_notification(request.cfg.secret, jids, notification)
+        server.send_notification(request.cfg.secrets['jabberbot'], jids, notification)
         return True
     except xmlrpclib.Error, err:
         logging.error("XML RPC error: %s" % str(err))
--- a/MoinMoin/events/notification.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/events/notification.py	Wed Feb 03 13:37:38 2010 +0100
@@ -52,8 +52,7 @@
     @param querystr: a dict passed to wikiutil.makeQueryString
 
     """
-    query = wikiutil.makeQueryString(querystr, True)
-    return request.getQualifiedURL(page.url(request, query))
+    return request.getQualifiedURL(page.url(request, querystr))
 
 def page_change_message(msgtype, request, page, lang, **kwargs):
     """Prepare a notification text for a page change of given type
@@ -128,12 +127,13 @@
     @return: a dict containing message body and subject
     """
     subject = _("New user account created on %(sitename)s") % {'sitename': sitename or "Wiki"}
-    text = _("""Dear Superuser, a new user has just been created. Details follow:
+    text = _("""Dear Superuser, a new user has just been created on %(sitename)s". Details follow:
 
     User name: %(username)s
     Email address: %(useremail)s""") % {
          'username': username,
          'useremail': email,
+         'sitename': sitename or "Wiki",
          }
 
     return {'subject': subject, 'text': text}
--- a/MoinMoin/events/xapian_index.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/events/xapian_index.py	Wed Feb 03 13:37:38 2010 +0100
@@ -61,7 +61,7 @@
         from MoinMoin.search.Xapian import Index
         index = Index(request)
         if index.exists():
-            index.update_page(request.page.page_name)
+            index.update_page(event.pagename)
 
 
 def handle(event):
--- a/MoinMoin/failure.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/failure.py	Wed Feb 03 13:37:38 2010 +0100
@@ -7,13 +7,13 @@
     @license: GNU GPL, see COPYING for details.
 """
 import sys, os
+import traceback
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
 
 from MoinMoin.support import cgitb
 from MoinMoin.error import ConfigurationError
-from traceback import extract_tb
 
 
 class View(cgitb.View):
@@ -69,7 +69,7 @@
         text = [self.formatExceptionMessage(self.info)]
 
         if self.info[0] == ConfigurationError:
-            tbt = extract_tb(self.info[1].exceptions()[-1][2])[-1]
+            tbt = traceback.extract_tb(self.info[1].exceptions()[-1][2])[-1]
             text.append(
                 f.paragraph('Error in your configuration file "%s"'
                             ' around line %d.' % tbt[:2]))
@@ -156,33 +156,44 @@
         raise err
 
     savedError = sys.exc_info()
-    logging.exception('An exception occured.')
+    logging.exception('An exception occurred, URI was "%s".' % request.request_uri)
+
     try:
-        debug = 'debug' in getattr(request, 'form', {})
+        display = request.cfg.traceback_show # might fail if we have no cfg yet
+    except:
         # default to True here to allow an admin setting up the wiki
         # to see the errors made in the configuration file
         display = True
-        logdir = None
-        if hasattr(request, 'cfg'):
-            display = request.cfg.traceback_show
-            logdir = request.cfg.traceback_log_dir
-        handler = cgitb.Hook(file=request, display=display, logdir=logdir,
-                             viewClass=View, debug=debug)
-        handler.handle()
+
+    try:
+        debug = 'debug' in request.form
     except:
+        debug = False
+
+    try:
+        # try to output a nice html error page
+        handler = cgitb.Hook(file=request, display=display, viewClass=View, debug=debug)
+        handler.handle(savedError)
+    except:
+        # if that fails, log the cgitb problem ...
+        logging.exception('cgitb raised this exception')
+        # ... and try again with a simpler output method:
         request.write('<pre>\n')
-        printTextException(request, savedError)
+        printTextException(request, savedError, display)
         request.write('\nAdditionally cgitb raised this exception:\n')
-        printTextException(request)
+        printTextException(request, display=display)
         request.write('</pre>\n')
 
 
-def printTextException(request, info=None):
+
+def printTextException(request, info=None, display=True):
     """ Simple text exception that should never fail
 
     Print all exceptions in a composite error.
     """
-    import traceback
+    if not display:
+        request.write("(Traceback display forbidden by configuration)\n")
+        return
     from MoinMoin import wikiutil
     if info is None:
         info = sys.exc_info()
--- a/MoinMoin/filter/__init__.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/filter/__init__.py	Wed Feb 03 13:37:38 2010 +0100
@@ -2,11 +2,12 @@
 """
     MoinMoin - Filter Package
 
-    @copyright: 2006-2008 MoinMoin:ThomasWaldmann
+    @copyright: 2006-2009 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
 import sys, os
+import time
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
@@ -17,6 +18,23 @@
 
 standard_codings = ['utf-8', 'iso-8859-15', 'iso-8859-1', ]
 
+try:
+    # requires Python >= 2.4
+    from MoinMoin.util.SubProcess import exec_cmd
+except ImportError:
+    # Note: remove this after requiring 2.4 for moin
+    def exec_cmd(cmd, input=None, timeout=None):
+        child_stdin, child_stdout = os.popen2(cmd)
+        # Note: we do NOT use popen3 because it would cause
+        #       deadlocks with such simple code as below:
+        if input:
+            child_stdin.write(input)
+        data = child_stdout.read()
+        child_stdout.close()
+        errors = '' # we don't have it (see note above about popen3)
+        rc = 0 # we don't have it, TODO: check if we can get it somehow
+        return data, errors, rc
+
 
 def quote_filename(filename):
     """ quote a filename (could contain blanks or other special chars) in a
@@ -38,12 +56,8 @@
         does not throw an exception or force ascii
     """
     filter_cmd = cmd % quote_filename(filename)
-    child_stdin, child_stdout, child_stderr = os.popen3(filter_cmd)
-    data = child_stdout.read()
-    errors = child_stderr.read()
-    child_stdout.close()
-    child_stderr.close()
-    logging.debug("cmd: %s stderr: %s" % (filter_cmd, errors))
+    data, errors, rc = exec_cmd(filter_cmd, timeout=300)
+    logging.debug("Command '%s', rc: %d, stdout: %d bytes, stderr: %s" % (filter_cmd, rc, len(data), errors))
     for c in codings:
         try:
             return data.decode(c)
--- a/MoinMoin/filter/application_pdf.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/filter/application_pdf.py	Wed Feb 03 13:37:38 2010 +0100
@@ -2,7 +2,9 @@
 """
     MoinMoin - PDF filter
 
-    Depends on: pdftotext command from xpdf-utils package
+    Depends on: pdftotext command from either xpdf-utils or poppler-utils
+                or any other package that provides a pdftotext command that
+                is callable with: pdftotext -enc UTF-8 filename.pdf -
 
     @copyright: 2006 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
@@ -11,5 +13,9 @@
 from MoinMoin.filter import execfilter
 
 def execute(indexobj, filename):
-    return execfilter("pdftotext -enc UTF-8 %s -", filename)
+    # using -q switch to get quiet operation (no messages, no errors),
+    # because poppler-utils pdftotext on Debian/Etch otherwise generates
+    # lots of output on stderr (e.g. 10MB stderr output) and that causes
+    # problems in current execfilter implementation.
+    return execfilter("pdftotext -q -enc UTF-8 %s -", filename)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/filter/application_vnd_ms_powerpoint.py	Wed Feb 03 13:37:38 2010 +0100
@@ -0,0 +1,16 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - mspowerpoint filter
+
+    Depends on: "catppt" command from "catdoc" package
+
+    @copyright: 2009 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin.filter import execfilter
+
+def execute(indexobj, filename):
+    data = execfilter("catppt -dutf-8 %s", filename)
+    return data
+
--- a/MoinMoin/filter/application_vnd_oasis_opendocument.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/filter/application_vnd_oasis_opendocument.py	Wed Feb 03 13:37:38 2010 +0100
@@ -4,7 +4,7 @@
 
     Depends on: nothing (only python with zlib)
 
-    @copyright: 2006 MoinMoin:ThomasWaldmann
+    @copyright: 2006-2009 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -21,8 +21,13 @@
         data = zf.read("content.xml")
         zf.close()
         data = " ".join(rx_stripxml.sub(" ", data).split())
-    except RuntimeError, err:
-        logging.error(str(err))
+    except (zipfile.BadZipfile, RuntimeError), err:
+        logging.error("%s [%s]" % (str(err), filename))
         data = ""
-    return data.decode('utf-8')
+    try:
+        data = data.decode('utf-8')
+    except UnicodeDecodeError:
+        # protected with password? no valid OpenDocument file?
+        data = u''
+    return data
 
--- a/MoinMoin/filter/application_vnd_sun_xml.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/filter/application_vnd_sun_xml.py	Wed Feb 03 13:37:38 2010 +0100
@@ -4,7 +4,7 @@
 
     Depends on: nothing (only python with zlib)
 
-    @copyright: 2006 MoinMoin:ThomasWaldmann
+    @copyright: 2006-2009 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -21,8 +21,13 @@
         data = zf.read("content.xml")
         zf.close()
         data = " ".join(rx_stripxml.sub(" ", data).split())
-    except RuntimeError, err:
-        logging.error(str(err))
+    except (zipfile.BadZipfile, RuntimeError), err:
+        logging.error("%s [%s]" % (str(err), filename))
         data = ""
-    return data.decode('utf-8')
+    try:
+        data = data.decode('utf-8')
+    except UnicodeDecodeError:
+        # protected with password? no valid OpenOffice file?
+        data = u''
+    return data
 
--- a/MoinMoin/formatter/__init__.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/formatter/__init__.py	Wed Feb 03 13:37:38 2010 +0100
@@ -14,7 +14,6 @@
 
 from MoinMoin.util import pysupport
 from MoinMoin import wikiutil
-from MoinMoin.support.python_compatibility import rsplit
 
 modules = pysupport.getPackageModules(__file__)
 
@@ -106,9 +105,6 @@
         """
         wikitag, wikiurl, wikitail, wikitag_bad = wikiutil.resolve_interwiki(self.request, interwiki, pagename)
         if wikitag == 'Self' or wikitag == self.request.cfg.interwikiname:
-            if '#' in wikitail:
-                wikitail, kw['anchor'] = rsplit(wikitail, '#', 1)
-                wikitail = wikiutil.url_unquote(wikitail)
             return self.pagelink(on, wikitail, **kw)
         return ''
 
--- a/MoinMoin/formatter/_tests/test_formatter.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/formatter/_tests/test_formatter.py	Wed Feb 03 13:37:38 2010 +0100
@@ -14,6 +14,7 @@
 
 class TestFormatter:
     def testSyntaxReferenceDomXml(self):
+        py.test.skip("domxml <p> generation is broken")
         f_name = 'dom_xml'
         try:
             formatter = wikiutil.importPlugin(self.request.cfg, "formatter", f_name, "Formatter")
@@ -21,7 +22,7 @@
             pass
         else:
             print "Formatting using %r" % formatter
-            self.formatPage("SyntaxReference", formatter)
+            self.formatPage("HelpOnMoinWikiSyntax", formatter)
             print "Done."
 
     def testSyntaxReferenceDocBook(self):
@@ -57,7 +58,7 @@
                 pass
             else:
                 print "Formatting using %r" % formatter
-                self.formatPage("SyntaxReference", formatter)
+                self.formatPage("HelpOnMoinWikiSyntax", formatter)
                 print "Done."
 
     def formatPage(self, pagename, formatter):
@@ -68,6 +69,7 @@
         self.request.reset()
         page = Page(self.request, pagename, formatter=formatter)
         self.request.formatter = page.formatter = formatter(self.request)
+        self.request.page = page
         #page.formatter.setPage(page)
         #page.hilite_re = None
 
--- a/MoinMoin/formatter/text_gedit.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/formatter/text_gedit.py	Wed Feb 03 13:37:38 2010 +0100
@@ -25,7 +25,7 @@
             self._base_depth = depth
 
         count_depth = max(depth - (self._base_depth - 1), 1)
-        heading_depth = depth + 1
+        heading_depth = depth
 
         # closing tag, with empty line after, to make source more readable
         if not on:
@@ -68,10 +68,15 @@
             return '<span style="background-color:#ffff11">{{attachment:%s|%s}}</span>' % (url, text)
 
     def attachment_link(self, on, url=None, **kw):
+        assert on in (0, 1, False, True) # make sure we get called the new way, not like the 1.5 api was
         _ = self.request.getText
+        querystr = kw.get('querystr', {})
+        assert isinstance(querystr, dict) # new in 1.6, only support dicts
+        if 'do' not in querystr:
+            querystr['do'] = 'view'
         if on:
             pagename = self.page.page_name
-            target = AttachFile.getAttachUrl(pagename, url, self.request)
+            target = AttachFile.getAttachUrl(pagename, url, self.request, do=querystr['do'])
             return self.url(on, target, title="attachment:%s" % wikiutil.quoteWikinameURL(url))
         else:
             return self.url(on)
@@ -234,3 +239,9 @@
     def line_anchorlink(self, on, lineno=0):
         return '' # not needed for gui editor feeding
 
+    def span(self, on, **kw):
+        previous_state = self.request.user.show_comments
+        self.request.user.show_comments = True
+        ret = text_html.Formatter.span(self, on, **kw)
+        self.request.user.show_comments = previous_state
+        return ret
--- a/MoinMoin/formatter/text_html.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/formatter/text_html.py	Wed Feb 03 13:37:38 2010 +0100
@@ -14,7 +14,7 @@
 from MoinMoin import wikiutil, i18n
 from MoinMoin.Page import Page
 from MoinMoin.action import AttachFile
-from MoinMoin.support.python_compatibility import set, rsplit
+from MoinMoin.support.python_compatibility import set
 
 # insert IDs into output wherever they occur
 # warning: breaks toggle line numbers javascript
@@ -488,8 +488,6 @@
         wikitag, wikiurl, wikitail, wikitag_bad = wikiutil.resolve_interwiki(self.request, interwiki, pagename)
         wikiurl = wikiutil.mapURL(self.request, wikiurl)
         if wikitag == 'Self': # for own wiki, do simple links
-            if '#' in wikitail:
-                wikitail, kw['anchor'] = rsplit(wikitail, '#', 1)
             wikitail = wikiutil.url_unquote(wikitail)
             try: # XXX this is the only place where we access self.page - do we need it? Crashes silently on actions!
                 pagename = wikiutil.AbsPageName(self.page.page_name, wikitail)
@@ -502,6 +500,9 @@
                 if querystr:
                     separator = ('?', '&')['?' in href]
                     href = '%s%s%s' % (href, separator, wikiutil.makeQueryString(querystr))
+                anchor = kw.get('anchor')
+                if anchor:
+                    href = '%s#%s' % (href, self.sanitize_to_id(anchor))
                 if wikitag_bad:
                     html_class = 'badinterwiki'
                 else:
@@ -601,7 +602,7 @@
 
     # Attachments ######################################################
 
-    def attachment_link(self, on, url=None, **kw):
+    def attachment_link(self, on, url=None, querystr=None, **kw):
         """ Link to an attachment.
 
             @param on: 1/True=start link, 0/False=end link
@@ -609,7 +610,8 @@
         """
         assert on in (0, 1, False, True) # make sure we get called the new way, not like the 1.5 api was
         _ = self.request.getText
-        querystr = kw.get('querystr', {})
+        if querystr is None:
+            querystr = {}
         assert isinstance(querystr, dict) # new in 1.6, only support dicts
         if 'do' not in querystr:
             querystr['do'] = 'view'
@@ -619,13 +621,14 @@
             fname = wikiutil.taintfilename(filename)
             if AttachFile.exists(self.request, pagename, fname):
                 target = AttachFile.getAttachUrl(pagename, fname, self.request, do=querystr['do'])
-                title = "attachment:%s" % url
-                css = 'attachment'
+                if not 'title' in kw:
+                    kw['title'] = "attachment:%s" % url
+                kw['css'] = 'attachment'
             else:
                 target = AttachFile.getAttachUrl(pagename, fname, self.request, upload=True)
-                title = _('Upload new attachment "%(filename)s"') % {'filename': fname}
-                css = 'attachment nonexistent'
-            return self.url(on, target, css=css, title=title)
+                kw['title'] = _('Upload new attachment "%(filename)s"') % {'filename': fname}
+                kw['css'] = 'attachment nonexistent'
+            return self.url(on, target, **kw)
         else:
             return self.url(on)
 
@@ -652,6 +655,7 @@
             return self.url(1, target, css=css, title=title) + img + self.url(0)
 
     def attachment_drawing(self, url, text, **kw):
+        # XXX text arg is unused!
         _ = self.request.getText
         pagename, filename = AttachFile.absoluteName(url, self.page.page_name)
         fname = wikiutil.taintfilename(filename)
@@ -670,42 +674,42 @@
         # check whether attachment exists, possibly point to upload form
         drawing_url = AttachFile.getAttachUrl(pagename, fname, self.request, drawing=drawing, upload=True)
         if not exists:
-            linktext = _('Create new drawing "%(filename)s (opens in new window)"')
-            return (self.url(1, drawing_url) +
-                    self.text(linktext % {'filename': fname}) +
-                    self.url(0))
+            title = _('Create new drawing "%(filename)s (opens in new window)"') % {'filename': fname}
+            img = self.icon('attachimg')  # TODO: we need a new "drawimg" in similar grey style and size
+            css = 'nonexistent'
+            return self.url(1, drawing_url, css=css, title=title) + img + self.url(0)
+
+        title = _('Edit drawing %(filename)s (opens in new window)') % {'filename': self.text(fname)}
+        kw['src'] = AttachFile.getAttachUrl(pagename, filename, self.request, addts=1)
+        kw['css'] = 'drawing'
 
         mappath = AttachFile.getFilename(self.request, pagename, drawing + u'.map')
-
-        # check for map file
-        if os.path.exists(mappath):
+        try:
+            map = file(mappath, 'r').read()
+        except (IOError, OSError):
+            map = ''
+        if map:
             # we have a image map. inline it and add a map ref to the img tag
-            try:
-                map = file(mappath, 'r').read()
-            except IOError:
-                pass
-            except OSError:
-                pass
-            else:
-                mapid = 'ImageMapOf' + drawing
-                # replace MAPNAME
-                map = map.replace('%MAPNAME%', mapid)
-                # add alt and title tags to areas
-                map = re.sub('href\s*=\s*"((?!%TWIKIDRAW%).+?)"', r'href="\1" alt="\1" title="\1"', map)
-                # add in edit links plus alt and title attributes
-                alt = title = _('Edit drawing %(filename)s (opens in new window)') % {'filename': self.text(fname)}
-                map = map.replace('%TWIKIDRAW%"', '%s" alt="%s" title="%s"' % (drawing_url, alt, title))
-                # unxml, because 4.01 concrete will not validate />
-                map = map.replace('/>', '>')
-                alt = title = _('Clickable drawing: %(filename)s') % {'filename': self.text(fname)}
-                src = AttachFile.getAttachUrl(pagename, filename, self.request, addts=1)
-                return (map + self.image(alt=alt, title=title, src=src, usemap='#'+mapid, css="drawing"))
+            mapid = 'ImageMapOf' + drawing
+            map = map.replace('%MAPNAME%', mapid)
+            # add alt and title tags to areas
+            map = re.sub('href\s*=\s*"((?!%TWIKIDRAW%).+?)"', r'href="\1" alt="\1" title="\1"', map)
+            map = map.replace('%TWIKIDRAW%"', '%s" alt="%s" title="%s"' % (drawing_url, title, title))
+            # unxml, because 4.01 concrete will not validate />
+            map = map.replace('/>', '>')
+            title = _('Clickable drawing: %(filename)s') % {'filename': self.text(fname)}
+            if 'title' not in kw:
+                kw['title'] = title
+            if 'alt' not in kw:
+                kw['alt'] = kw['title']
+            kw['usemap'] = '#'+mapid
+            return map + self.image(**kw)
         else:
-            alt = title = _('Edit drawing %(filename)s (opens in new window)') % {'filename': self.text(fname)}
-            src = AttachFile.getAttachUrl(pagename, filename, self.request, addts=1)
-            return (self.url(1, drawing_url) +
-                    self.image(alt=alt, title=title, src=src, css="drawing") +
-                    self.url(0))
+            if 'title' not in kw:
+                kw['title'] = title
+            if 'alt' not in kw:
+                kw['alt'] = kw['title']
+            return self.url(1, drawing_url) + self.image(**kw) + self.url(0)
 
 
     # Text ##############################################################
--- a/MoinMoin/formatter/text_html_percent.py	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/formatter/text_html_percent.py	Wed Feb 03 13:37:38 2010 +0100
@@ -17,7 +17,7 @@
 class Formatter(TextHtmlFormatter):
 
     def _open(self, tag, newline=False, attr=None, allowed_attrs=None, **kw):
-        """ Escape % signs in tags, see also text_html.Formatter._open. """
+        """ Escape % characters in tags, see also text_html.Formatter._open. """
         tagstr = TextHtmlFormatter._open(self, tag, newline, attr, allowed_attrs, **kw)
         return tagstr.replace('%', '%%')
 
--- a/MoinMoin/i18n/MoinMoin.pot	Wed Feb 03 13:35:28 2010 +0100
+++ b/MoinMoin/i18n/MoinMoin.pot	Wed Feb 03 13:37:38 2010 +0100
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2008-09-07 23:23+0200\n"
+"POT-Creation-Date: 2009-12-05 17:32+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -16,66 +16,16 @@
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#, python-format
-msgid "Argument \"%s\" must be a boolean value, not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument must be a boolean value, not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument \"%s\" must be an integer value, not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument must be an integer value, not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument \"%s\" must be a floating point value, not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument must be a floating point value, not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument \"%s\" must be a complex value, not \"%s\""
+msgid "The wiki is currently not reachable."
+msgstr ""
+
+msgid "Invalid username or password."
 msgstr ""
 
 #, python-format
-msgid "Argument must be a complex value, not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument \"%s\" must be one of \"%s\", not \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Argument must be one of \"%s\", not \"%s\""
-msgstr ""
-
-msgid "Too many arguments"
-msgstr ""
-
-msgid "Cannot have arguments without name following named arguments"
-msgstr ""
-
-#, python-format
-msgid "Argument \"%s\" is required"
-msgstr ""
-
-#, python-format
-msgid "No argument named \"%s\""
-msgstr ""
-
-#, python-format
-msgid "Expected \"=\" to follow \"%(token)s\""
-msgstr ""
-
-#, python-format
-msgid "Expected a value for key \"%(token)s\""
+msgid ""
+"The remote wiki uses a different InterWiki name (%(remotename)s) internally "
+"than you specified (%(localname)s)."
 msgstr ""
 
 msgid "Your changes are not saved!"
@@ -324,45 +274,24 @@
 msgstr ""
 
 #, python-format
-msgid "Invalid highlighting regular expression \"%(regex)s\": %(error)s"
+msgid ""
+"Login Name: %s\n"
+"\n"
+"Password recovery token: %s\n"
+"\n"
+"Password reset URL: %s/?action=recoverpass&name=%s&token=%s\n"
 msgstr ""
 
 msgid ""
-"The backed up content of this page is deprecated and will not be included in "
-"search results!"
-msgstr ""
-
-#, python-format
-msgid "Revision %(rev)d as of %(date)s"
-msgstr ""
-
-#, python-format
-msgid "Redirected from page \"%(page)s\""
+"Somebody has requested to email you a password recovery token.\n"
+"\n"
+"If you lost your password, please go to the password reset URL below or\n"
+"go to the password recovery page again and enter your username and the\n"
+"recovery token.\n"
 msgstr ""
 
 #, python-format
-msgid "This page redirects to page \"%(page)s\""
-msgstr ""
-
-msgid "Create New Page"
-msgstr ""
-
-msgid "You are not allowed to view this page."
-msgstr ""
-
-msgid "The wiki is currently not reachable."
-msgstr ""
-
-msgid "Invalid username or password."
-msgstr ""
-
-#, python-format
-msgid ""
-"The remote wiki uses a different InterWiki name (%(remotename)s) internally "
-"than you specified (%(localname)s)."
-msgstr ""
-
-msgid "Text mode"
+msgid "[%(sitename)s] Your wiki account data"
 msgstr ""
 
 #, python-format
@@ -401,25 +330,308 @@
 msgid "The file %s was not found in the package."
 msgstr ""
 
+msgid "Text mode"
+msgstr ""
+
 #, python-format
-msgid ""
-"Login Name: %s\n"
-"\n"
-"Password recovery token: %s\n"
-"\n"
-"Password reset URL: %s/?action=recoverpass&name=%s&token=%s\n"
+msgid "Argument \"%s\" must be a boolean value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument must be a boolean value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument \"%s\" must be an integer value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument must be an integer value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument \"%s\" must be a floating point value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument must be a floating point value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument \"%s\" must be a complex value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument must be a complex value, not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument \"%s\" must be one of \"%s\", not \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Argument must be one of \"%s\", not \"%s\""
+msgstr ""
+
+msgid "Too many arguments"
+msgstr ""
+
+msgid "Cannot have arguments without name following named arguments"
+msgstr ""
+
+#, python-format
+msgid "Argument \"%s\" is required"
+msgstr ""
+
+#, python-format
+msgid "No argument named \"%s\""
+msgstr ""
+
+#, python-format
+msgid "Expected \"=\" to follow \"%(token)s\""
+msgstr ""
+
+#, python-format
+msgid "Expected a value for key \"%(token)s\""
+msgstr ""
+
+#, python-format
+msgid "Invalid highlighting regular expression \"%(regex)s\": %(error)s"
 msgstr ""
 
 msgid ""
-"Somebody has requested to email you a password recovery token.\n"
-"\n"
-"If you lost your password, please go to the password reset URL below or\n"
-"go to the password recovery page again and enter your username and the\n"
-"recovery token.\n"
+"The backed up content of this page is deprecated and will not be included in "
+"search results!"
 msgstr ""
 
 #, python-format
-msgid "[%(sitename)s] Your wiki account data"
+msgid "Revision %(rev)d as of %(date)s"
+msgstr ""
+
+#, python-format
+msgid "Redirected from page \"%(page)s\""
+msgstr ""
+
+#, python-format
+msgid "This page redirects to page \"%(page)s\""
+msgstr ""
+
+msgid "Create New Page"
+msgstr ""
+
+msgid "You are not allowed to view this page."
+msgstr ""
+
+msgid "Switch user"
+msgstr ""
+
+msgid "No user selected"
+msgstr ""
+
+msgid ""
+"You can now change the settings of the selected user account; log out to get "
+"back to your account."
+msgstr ""
+
+msgid "You are the only user."
+msgstr ""
+
+msgid ""
+"As a superuser, you can temporarily assume the identity of another user."
+msgstr ""
+
+msgid "Select User"
+msgstr ""
+
+msgid "Change password"
+msgstr ""
+
+msgid "Passwords don't match!"
+msgstr ""
+
+msgid "Please specify a password!"
+msgstr ""
+
+#, python-format
+msgid "Password not acceptable: %s"
+msgstr ""
+
+msgid "Your password has been changed."
+msgstr ""
+
+msgid "To change your password, enter a new password twice."
+msgstr ""
+
+msgid "Password"
+msgstr ""
+
+msgid "Password repeat"
+msgstr ""
+
+msgid "OpenID settings"
+msgstr ""
+
+msgid "Cannot remove all OpenIDs."
+msgstr ""
+
+msgid "The selected OpenIDs have been removed."
+msgstr ""
+
+msgid "No OpenID given."
+msgstr ""
+
+msgid "OpenID is already present."