changeset 1932:2bd864ed640e namespaces

merged default branch into namespaces branch
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sun, 03 Feb 2013 03:56:51 +0100
parents 39151f399b59 (current diff) 7dc5085f4960 (diff)
children 4658b5d59ad6
files .hgignore MoinMoin/_tests/__init__.py MoinMoin/_tests/test_user.py MoinMoin/app.py MoinMoin/apps/admin/views.py MoinMoin/apps/frontend/views.py MoinMoin/auth/_tests/test_http.py MoinMoin/config/default.py MoinMoin/items/__init__.py MoinMoin/items/content.py MoinMoin/script/migration/moin19/import19.py MoinMoin/storage/__init__.py MoinMoin/storage/middleware/_tests/test_indexing.py MoinMoin/storage/middleware/_tests/test_protecting.py MoinMoin/storage/middleware/indexing.py MoinMoin/storage/middleware/protecting.py MoinMoin/templates/global_history.html MoinMoin/templates/history.html MoinMoin/templates/item_link_list.html MoinMoin/templates/layout.html MoinMoin/templates/search.html MoinMoin/themes/__init__.py MoinMoin/user.py MoinMoin/util/_tests/test_md5crypt.py MoinMoin/util/md5crypt.py
diffstat 88 files changed, 827 insertions(+), 1101 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sat Dec 01 15:20:52 2012 +0100
+++ b/.hgignore	Sun Feb 03 03:56:51 2013 +0100
@@ -22,6 +22,8 @@
 ^.settings
 ^MANIFEST$
 .DS_Store
+^\.cache/
+^\.idea/
 .sqlite$
 .orig$
 .rej$
--- a/MoinMoin/_tests/__init__.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/_tests/__init__.py	Sun Feb 03 03:56:51 2013 +0100
@@ -57,7 +57,7 @@
         meta[NAME] = [name, ]
     if CONTENTTYPE not in meta:
         meta[CONTENTTYPE] = u'application/octet-stream'
-    rev = item.store_revision(meta, StringIO(data))
+    rev = item.store_revision(meta, StringIO(data), return_rev=True)
     return rev
 
 def create_random_string_list(length=14, count=10):
--- a/MoinMoin/_tests/test_error.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/_tests/test_error.py	Sun Feb 03 03:56:51 2013 +0100
@@ -30,9 +30,11 @@
 
     def testCreateWithObject(self):
         """ error: create with any object """
-        class Foo:
+
+        class Foo(object):
             def __unicode__(self):
                 return u'טעות'
+
             def __str__(self):
                 return 'טעות'
 
@@ -44,7 +46,7 @@
         """ error: access error like a dict """
         test = 'value'
         err = error.Error(test)
-        assert '%(message)s' % err == test
+        assert '%(message)s' % dict(message=err) == test
 
 class TestCompositeError(object):
 
--- a/MoinMoin/_tests/test_user.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/_tests/test_user.py	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # Copyright: 2003-2004 by Juergen Hermann <jh@web.de>
 # Copyright: 2009 by ReimarBauer
+# Copyright: 2013 by ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
@@ -38,9 +39,7 @@
         assert u.exists()
 
 
-class TestLoginWithPassword(object):
-    """user: login tests"""
-
+class TestUser(object):
     def setup_method(self, method):
         # Save original user
         self.saved_user = flaskg.user
@@ -56,6 +55,8 @@
         # Restore original user
         flaskg.user = self.saved_user
 
+    # Passwords / Login -----------------------------------------------
+
     def testAsciiPassword(self):
         """ user: login with ascii password """
         # Create test user
@@ -78,107 +79,50 @@
         theUser = user.User(name=name, password=password)
         assert theUser.valid
 
-    def test_auth_with_ssha_stored_password(self):
+    def testInvalidatePassword(self):
+        """ user: test invalidation of password """
+        # Create test user
+        name = u'__Non Existent User Name__'
+        password = name
+        self.createUser(name, password)
+
+        # Try to "login"
+        theUser = user.User(name=name, password=password)
+        assert theUser.valid
+
+        # invalidate the stored password (hash)
+        theUser.set_password("") # emptry str or None means "invalidate"
+        theUser.save()
+
+        # Try to "login" with previous password
+        theUser = user.User(name=name, password=password)
+        assert not theUser.valid
+
+        # Try to "login" with empty password
+        theUser = user.User(name=name, password="")
+        assert not theUser.valid
+
+    def testPasswordHash(self):
         """
-        Create user with {SSHA} password and check that user can login.
+        Create user, set a specific pw hash and check that user can login
+        with the correct password and can not log in with a wrong password.
         """
 
         # Create test user
         name = u'Test User'
-        # pass = 12345
-        # salt = salt
-        password = '{SSHA}x4YEGdfI4i0qROaY3NTHCmwSJY5zYWx0'
-        self.createUser(name, password, True)
-
-        # Try to "login"
-        theuser = user.User(name=name, password='12345')
-        assert theuser.valid
+        # sha512_crypt passlib hash for '12345':
+        pw_hash = '$6$rounds=1001$y9ObPHKb8cvRCs5G$39IW1i5w6LqXPRi4xqAu3OKv1UOpVKNkwk7zPnidsKZWqi1CrQBpl2wuq36J/s6yTxjCnmaGzv/2.dAmM8fDY/'
+        self.createUser(name, pw_hash, True)
 
-    def test_auth_with_apr1_stored_password(self):
-        """
-        Create user with {APR1} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbm blaze 12345"
-        password = '{APR1}$apr1$NG3VoiU5$PSpHT6tV0ZMKkSZ71E3qg.' # 12345
-        self.createUser(name, password, True)
-
-        # Try to "login"
-        theuser = user.User(name=name, password='12345')
-        assert theuser.valid
-
-    def test_auth_with_md5_stored_password(self):
-        """
-        Create user with {MD5} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        password = '{MD5}$1$salt$etVYf53ma13QCiRbQOuRk/' # 12345
-        self.createUser(name, password, True)
-
-        # Try to "login"
+        # Try to "login" with correct password
         theuser = user.User(name=name, password='12345')
         assert theuser.valid
 
-    def test_auth_with_des_stored_password(self):
-        """
-        Create user with {DES} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbd blaze 12345"
-        password = '{DES}gArsfn7O5Yqfo' # 12345
-        self.createUser(name, password, True)
-
-        try:
-            import crypt
-            # Try to "login"
-            theuser = user.User(name=name, password='12345')
-            assert theuser.valid
-        except ImportError:
-            pytest.skip("Platform does not provide crypt module!")
+        # Try to "login" with a wrong password
+        theuser = user.User(name=name, password='wrong')
+        assert not theuser.valid
 
-    def test_auth_with_ssha256_stored_password(self):
-        """
-        Create user with {SSHA256} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with online sha256 tool
-        # pass: 12345
-        # salt: salt
-        # base64 encoded
-        password = '{SSHA256}r4ONZUfEyn9MUkcyDQkQ5MBNpdIerM24MasxFpuQBaFzYWx0'
-
-        self.createUser(name, password, True)
-
-        # Try to "login"
-        theuser = user.User(name=name, password='12345')
-        assert theuser.valid
-
-    def test_regression_user_password_started_with_sha(self):
-        # This is regression test for bug in function 'user.create_user'.
-        #
-        # This function does not encode passwords which start with '{SHA}'
-        # It treats them as already encoded SHA hashes.
-        #
-        # If user during registration specifies password starting with '{SHA}'
-        # this password will not get encoded and user object will get saved with empty enc_password
-        # field.
-        #
-        # Such situation leads to "KeyError: 'enc_password'" during
-        # user authentication.
-
-        # Any Password begins with the {SHA} symbols led to
-        # "KeyError: 'enc_password'" error during user authentication.
-        user_name = u'moin'
-        user_password = u'{SHA}LKM56'
-        user.create_user(user_name, user_password, u'moin@moinmo.in', u'')
-
-        # Try to "login"
-        theuser = user.User(name=user_name, password=user_password)
-        assert theuser.valid
+    # Subscriptions ---------------------------------------------------
 
     def testSubscriptionSubscribedPage(self):
         """ user: tests is_subscribed_to  """
@@ -203,72 +147,6 @@
         theUser.subscribe(pagename)
         assert not theUser.is_subscribed_to([testPagename]) # list(!) of pages to check
 
-    def test_upgrade_password_from_ssha_to_ssha256(self):
-        """
-        Create user with {SSHA} password and check that logging in
-        upgrades to {SSHA256}.
-        """
-        name = u'/no such user/'
-        # pass = 'MoinMoin', salt = '12345'
-        password = '{SSHA}xkDIIx1I7A4gC98Vt/+UelIkTDYxMjM0NQ=='
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='MoinMoin')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_sha_to_ssha256(self):
-        """
-        Create user with {SHA} password and check that logging in
-        upgrades to {SSHA256}.
-        """
-        name = u'/no such user/'
-        password = '{SHA}jLIjfQZ5yojbZGTqxg2pY0VROWQ=' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_apr1_to_ssha256(self):
-        """
-        Create user with {APR1} password and check that logging in
-        upgrades to {SSHA256}.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbm blaze 12345"
-        password = '{APR1}$apr1$NG3VoiU5$PSpHT6tV0ZMKkSZ71E3qg.' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_md5_to_ssha256(self):
-        """
-        Create user with {MD5} password and check that logging in
-        upgrades to {SSHA}.
-        """
-        # Create test user
-        name = u'Test User'
-        password = '{MD5}$1$salt$etVYf53ma13QCiRbQOuRk/' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_des_to_ssha256(self):
-        """
-        Create user with {DES} password and check that logging in
-        upgrades to {SSHA}.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbd blaze 12345"
-        password = '{DES}gArsfn7O5Yqfo' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
     # Bookmarks -------------------------------------------------------
 
     def test_bookmark(self):
--- a/MoinMoin/_tests/wikiconfig.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/_tests/wikiconfig.py	Sun Feb 03 03:56:51 2013 +0100
@@ -25,3 +25,12 @@
     interwikiname = u'MoinTest'
     interwiki_map = dict(Self='http://localhost:8080/', MoinMoin='http://moinmo.in/')
     interwiki_map[interwikiname] = 'http://localhost:8080/'
+
+    passlib_crypt_context = dict(
+        schemes=["sha512_crypt", ],
+        # for the tests, we don't want to have varying rounds
+        sha512_crypt__vary_rounds=0,
+        # for the tests, we want to have a rather low rounds count,
+        # so the tests run quickly (do NOT use low counts in production!)
+        sha512_crypt__default_rounds=1001,
+    )
--- a/MoinMoin/app.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/app.py	Sun Feb 03 03:56:51 2013 +0100
@@ -100,8 +100,22 @@
     clock.stop('create_app load config')
     clock.start('create_app register')
     # register converters
-    from werkzeug.routing import PathConverter
-    app.url_map.converters['itemname'] = PathConverter
+    from werkzeug.routing import BaseConverter
+
+    class ItemNameConverter(BaseConverter):
+        """Like the default :class:`UnicodeConverter`, but it also matches
+        slashes (except at the beginning AND end).
+        This is useful for wikis and similar applications::
+
+            Rule('/<itemname:wikipage>')
+            Rule('/<itemname:wikipage>/edit')
+
+        :param map: the :class:`Map`.
+        """
+        regex = '[^/]+?(/[^/]+?)*'
+        weight = 200
+
+    app.url_map.converters['itemname'] = ItemNameConverter
     # register modules, before/after request functions
     from MoinMoin.apps.frontend import frontend
     frontend.before_request(before_wiki)
--- a/MoinMoin/apps/admin/templates/admin/index.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/admin/index.html	Sun Feb 03 03:56:51 2013 +0100
@@ -2,9 +2,9 @@
 {% block content %}
 <h1>{{ _("Admin Menu") }}</h1>
 <ul>
-    <li><a href="{{ url_for('admin.userbrowser') }}">{{ _("User Browser") }}</a></li>
-    <li><a href="{{ url_for('admin.sysitems_upgrade') }}">{{ _("Upgrade system items") }}</a></li>
+    <li><a href="{{ url_for('admin.userbrowser') }}">{{ _("Users") }}</a></li>
+    <li><a href="{{ url_for('admin.sysitems_upgrade') }}">{{ _("Upgrade System Items") }}</a></li>
     <li><a href="{{ url_for('admin.wikiconfig') }}">{{ _("Show Wiki Configuration") }}</a></li>
-    <li><a href="{{ url_for('admin.wikiconfighelp') }}">{{ _("Show Wiki Configuration Help") }}</a></li>
+    <li><a href="{{ url_for('admin.wikiconfighelp') }}">{{ _("Wiki Configuration Help") }}</a></li>
 </ul>
 {% endblock %}
--- a/MoinMoin/apps/admin/templates/admin/sysitems_upgrade.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/admin/sysitems_upgrade.html	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,6 @@
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("System items upgrade") }}</h1>
+<h1>{{ _("Upgrade System Items") }}</h1>
 <p>
 {{ _("You can upgrade your system items by uploading an xml file with new items below.") }}
 </p>
--- a/MoinMoin/apps/admin/templates/admin/userbrowser.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/admin/userbrowser.html	Sun Feb 03 03:56:51 2013 +0100
@@ -1,5 +1,6 @@
 {% extends theme("layout.html") %}
 {% block content %}
+    <h1>{{ _("Users") }}</h1>
     <table class="zebra">
     <tr>
         <th>{{ _("User name") }}</th>
--- a/MoinMoin/apps/admin/templates/admin/wikiconfig.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/admin/wikiconfig.html	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,6 @@
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("Wiki configuration") }}</h1>
+<h1>{{ _("Show Wiki Configuration") }}</h1>
 <p>
 {{ _("This table shows all settings in this wiki that do not have default values. "
      "Settings that the configuration system doesn't know about are shown in italic, "
@@ -11,7 +11,7 @@
 <table class="zebra">
 <thead>
 <tr>
-<th>{{ _('Variable name') }}</th>
+<th>{{ _('Variable Name') }}</th>
 <th>{{ _('Setting') }}</th>
 </tr>
 </thead>
--- a/MoinMoin/apps/admin/templates/admin/wikiconfighelp.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/admin/wikiconfighelp.html	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,6 @@
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("WikiConfig Help") }}</h1>
+<h1>{{ _("Wiki Configuration Help") }}</h1>
 {% for heading, desc, opts in groups %}
     <h2>{{ heading }}</h2>
     {% if desc %}
@@ -9,7 +9,7 @@
     <table class="zebra">
     <thead>
     <tr>
-        <th>{{ _('Variable name') }}</th>
+        <th>{{ _('Variable Name') }}</th>
         <th>{{ _('Default') }}</th>
         <th>{{ _('Description') }}</th>
     </tr>
--- a/MoinMoin/apps/admin/templates/user/highlighterhelp.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/user/highlighterhelp.html	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,6 @@
 {% import "utils.html" as utils %}
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("Available Highlighters") }}</h1>
+<h1>{{ _("Highlighters") }}</h1>
 {{ utils.table(headings, rows) }}
 {% endblock %}
--- a/MoinMoin/apps/admin/templates/user/index_user.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/user/index_user.html	Sun Feb 03 03:56:51 2013 +0100
@@ -6,10 +6,11 @@
 </ul>
 <h1>{{ _("User Menu") }}</h1>
 <ul>
+    <li><a href="{{ url_for('frontend.mychanges') }}">{{ _("My Changes") }}</a></li>
     <li><a href="{{ url_for('frontend.wanted_items') }}">{{ _("Wanted Items") }}</a></li>
     <li><a href="{{ url_for('frontend.orphaned_items') }}">{{ _("Orphaned Items") }}</a></li>
-    <li><a href="{{ url_for('admin.itemsize') }}">{{ _("Item sizes (latest revision)") }}</a></li>
-    <li><a href="{{ url_for('admin.interwikihelp') }}">{{ _("Known InterWiki names") }}</a></li>
-    <li><a href="{{ url_for('admin.highlighterhelp') }}">{{ _("Available Highlighters") }}</a></li>
+    <li><a href="{{ url_for('admin.itemsize') }}">{{ _("Item Sizes (last revision)") }}</a></li>
+    <li><a href="{{ url_for('admin.interwikihelp') }}">{{ _("InterWiki Names") }}</a></li>
+    <li><a href="{{ url_for('admin.highlighterhelp') }}">{{ _("Highlighters") }}</a></li>
 </ul>
 {% endblock %}
--- a/MoinMoin/apps/admin/templates/user/interwikihelp.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/user/interwikihelp.html	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,6 @@
 {% import "utils.html" as utils %}
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("Known InterWiki names") }}</h1>
-{{ utils.table(headings, rows) }}
+<h1>{{ _("InterWiki Names") }}</h1>
+{{ utils.table(headings, rows, url_cols=[2]) }}
 {% endblock %}
--- a/MoinMoin/apps/admin/templates/user/itemsize.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/templates/user/itemsize.html	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,6 @@
 {% import "utils.html" as utils %}
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("Item sizes (latest revision)") }}</h1>
-{{ utils.table(headings, rows) }}
+<h1>{{ _("Item Sizes (last revision)") }}</h1>
+{{ utils.table(headings, rows, itemname_cols=[2]) }}
 {% endblock %}
--- a/MoinMoin/apps/admin/views.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/admin/views.py	Sun Feb 03 03:56:51 2013 +0100
@@ -50,7 +50,7 @@
                           groups=[groupname for groupname in groups if rev.meta[NAME] in groups[groupname]],
                      )
                      for rev in revs]
-    return render_template('admin/userbrowser.html', user_accounts=user_accounts, title_name=_(u"User Browser"))
+    return render_template('admin/userbrowser.html', user_accounts=user_accounts, title_name=_(u"Users"))
 
 
 @admin.route('/userprofile/<user_name>', methods=['GET', 'POST', ])
@@ -161,7 +161,7 @@
 
     found.sort()
     return render_template('admin/wikiconfig.html',
-                           title_name=_(u"Wiki Configuration"),
+                           title_name=_(u"Show Wiki Configuration"),
                            found=found, settings=settings)
 
 
@@ -207,7 +207,7 @@
     rows = sorted([[desc, ' '.join(names), ' '.join(patterns), ' '.join(mimetypes), ]
                    for desc, names, patterns, mimetypes in lexers])
     return render_template('user/highlighterhelp.html',
-                           title_name=_(u"Highlighter Help"),
+                           title_name=_(u"Highlighters"),
                            headings=headings,
                            rows=rows)
 
@@ -220,7 +220,7 @@
                ]
     rows = sorted(app.cfg.interwiki_map.items())
     return render_template('user/interwikihelp.html',
-                           title_name=_(u"Interwiki Help"),
+                           title_name=_(u"Interwiki Names"),
                            headings=headings,
                            rows=rows)
 
@@ -235,6 +235,6 @@
             for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)]
     rows = sorted(rows, reverse=True)
     return render_template('user/itemsize.html',
-                           title_name=_(u"Item Size"),
+                           title_name=_(u"Item Sizes"),
                            headings=headings,
                            rows=rows)
--- a/MoinMoin/apps/frontend/views.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/apps/frontend/views.py	Sun Feb 03 03:56:51 2013 +0100
@@ -1,5 +1,5 @@
 # Copyright: 2012 MoinMoin:CheerXiao
-# Copyright: 2003-2010 MoinMoin:ThomasWaldmann
+# Copyright: 2003-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2011 MoinMoin:AkashSinha
 # Copyright: 2011 MoinMoin:ReimarBauer
 # Copyright: 2008 MoinMoin:FlorianKrupicka
@@ -359,6 +359,7 @@
     return item.do_show(rev)
 
 
+@frontend.route('/<itemname:item_name>/')  # note: unwanted trailing slash
 @frontend.route('/+show/<itemname:item_name>')
 def redirect_show_item(item_name):
     return redirect(url_for_item(item_name))
@@ -787,7 +788,7 @@
     :returns: a page with all the items which link or transclude item_name
     """
     my_changes = _mychanges(flaskg.user.itemid)
-    return render_template('item_link_list.html',
+    return render_template('link_list_no_item_panel.html',
                            title_name=_(u'My Changes'),
                            headline=_(u'My Changes'),
                            item_names=my_changes
@@ -818,7 +819,7 @@
     :returns: a page with all the items which link or transclude item_name
     """
     refs_here = _backrefs(item_name)
-    return render_template('item_link_list.html',
+    return render_template('link_list_item_panel.html',
                            item_name=item_name,
                            headline=_(u"Items which refer to '%(item_name)s'", item_name=item_name),
                            item_names=refs_here
@@ -926,7 +927,7 @@
     referred = linked | transcluded
     wanteds = referred - existing
     title_name = _(u'Wanted Items')
-    return render_template('item_link_list.html',
+    return render_template('link_list_no_item_panel.html',
                            headline=_(u'Wanted Items'),
                            title_name=title_name,
                            item_names=wanteds)
@@ -942,7 +943,7 @@
     referred = linked | transcluded
     orphans = existing - referred
     title_name = _('Orphaned Items')
-    return render_template('item_link_list.html',
+    return render_template('link_list_no_item_panel.html',
                            title_name=title_name,
                            headline=_(u'Orphaned Items'),
                            item_names=orphans)
@@ -1187,16 +1188,17 @@
     """Validator for a valid password recovery form
     """
     passwords_mismatch_msg = L_('The passwords do not match.')
-    password_encoding_problem_msg = L_('New password is unacceptable, encoding trouble.')
+    password_problem_msg = L_('New password is unacceptable, could not get processed.')
 
     def validate(self, element, state):
         if element['password1'].value != element['password2'].value:
             return self.note_error(element, state, 'passwords_mismatch_msg')
 
+        password = element['password1'].value
         try:
-            crypto.crypt_password(element['password1'].value)
-        except UnicodeError:
-            return self.note_error(element, state, 'password_encoding_problem_msg')
+            app.cfg.cache.pwd_context.encrypt(password)
+        except (ValueError, TypeError) as err:
+            return self.note_error(element, state, 'password_problem_msg')
 
         return True
 
@@ -1321,7 +1323,7 @@
     """
     passwords_mismatch_msg = L_('The passwords do not match.')
     current_password_wrong_msg = L_('The current password was wrong.')
-    password_encoding_problem_msg = L_('New password is unacceptable, encoding trouble.')
+    password_problem_msg = L_('New password is unacceptable, could not get processed.')
 
     def validate(self, element, state):
         if not (element['password_current'].valid and element['password1'].valid and element['password2'].valid):
@@ -1333,10 +1335,11 @@
         if element['password1'].value != element['password2'].value:
             return self.note_error(element, state, 'passwords_mismatch_msg')
 
+        password = element['password1'].value
         try:
-            crypto.crypt_password(element['password1'].value)
-        except UnicodeError:
-            return self.note_error(element, state, 'password_encoding_problem_msg')
+            app.cfg.cache.pwd_context.encrypt(password)
+        except (ValueError, TypeError) as err:
+            return self.note_error(element, state, 'password_problem_msg')
         return True
 
 
@@ -1663,7 +1666,7 @@
             rank = matches[name]
             if rank == wanted_rank:
                 item_names.append(name)
-    return render_template("item_link_list.html",
+    return render_template("link_list_item_panel.html",
                            headline=_("Items with similar names to '%(item_name)s'", item_name=item_name),
                            item_name=item_name, # XXX no item
                            item_names=item_names)
@@ -1888,7 +1891,7 @@
     query = And([Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, tag), ])
     revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
     item_names = [rev.name for rev in revs]
-    return render_template("item_link_list.html",
+    return render_template("link_list_no_item_panel.html",
                            headline=_("Items tagged with %(tag)s", tag=tag),
                            item_name=tag,
                            item_names=item_names)
--- a/MoinMoin/auth/_tests/test_http.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/auth/_tests/test_http.py	Sun Feb 03 03:56:51 2013 +0100
@@ -14,7 +14,8 @@
 
 class TestHTTPAuthMoin(object):
     """ Test: HTTPAuthMoin """
-    class Auth:
+
+    class Auth(object):
         def __init__(self):
             self.username = 'ValidUser'
             self.password = 'test_pass'
--- a/MoinMoin/config/default.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/config/default.py	Sun Feb 03 03:56:51 2013 +0100
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright: 2000-2004 Juergen Hermann <jh@web.de>
-# Copyright: 2005-2011 MoinMoin:ThomasWaldmann
+# Copyright: 2005-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2008      MoinMoin:JohannesBerg
 # Copyright: 2010      MoinMoin:DiogenesAugusto
 # Copyright: 2011      MoinMoin:AkashSinha
@@ -155,6 +155,12 @@
                 raise error.ConfigurationError("You must set a (at least {0} chars long) secret string for secrets['{1}']!".format(
                     secret_min_length, secret_key_name))
 
+        from passlib.context import CryptContext
+        try:
+            self.cache.pwd_context = CryptContext(**self.passlib_crypt_context)
+        except ValueError as err:
+            raise error.ConfigurationError("passlib_crypt_context configuration is invalid [{0}].".format(err))
+
     def _config_check(self):
         """ Check namespace and warn about unknown names
 
@@ -293,7 +299,7 @@
 #
 options_no_group_name = {
   # ==========================================================================
-  'datastruct': ('Datastruct settings', None, (
+  'datastruct': ('Datastruct', None, (
     #('dicts', lambda cfg: datastruct.ConfigDicts({}),
     ('dicts', lambda cfg: datastruct.WikiDicts(),
      "function f(cfg) that returns a backend which is used to access dicts definitions."),
@@ -302,7 +308,7 @@
      "function f(cfg) that returns a backend which is used to access groups definitions."),
   )),
   # ==========================================================================
-  'auth': ('Authentication / Authorization / Security settings', None, (
+  'auth': ('Authentication / Authorization / Security', None, (
     ('auth', DefaultExpression('[MoinAuth()]'),
      "list of auth objects, to be called in this order (see HelpOnAuthentication)"),
     ('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."""),
@@ -315,9 +321,25 @@
 
     ('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`)'),
+
+    ('passlib_crypt_context', dict(
+        # schemes we want to support (or deprecated schemes for which we still have
+        # hashes in our storage).
+        # note about bcrypt: it needs additional code (that is not pure python and
+        # thus either needs compiling or installing platform-specific binaries)
+        schemes=["sha512_crypt", ],
+        # default scheme for creating new pw hashes (if not given, passlib uses first from schemes)
+        #default="sha512_crypt",
+        # deprecated schemes get auto-upgraded to the default scheme at login
+        # time or when setting a password (including doing a moin account pwreset).
+        #deprecated=["auto"],
+        # vary rounds parameter randomly when creating new hashes...
+        #all__vary_rounds=0.1,
+     ),
+     "passlib CryptContext arguments, see passlib docs"),
   )),
   # ==========================================================================
-  'spam_leech_dos': ('Anti-Spam/Leech/DOS',
+  'spam_leech_dos': ('Anti-Spam / Leech / DOS',
   'These settings help limiting ressource usage and avoiding abuse.',
   (
     ('textchas', None,
@@ -326,7 +348,7 @@
      "Time [s] for a !TextCha to expire."),
   )),
   # ==========================================================================
-  'style': ('Style / Theme / UI related',
+  'style': ('Style / Theme / UI',
   'These settings control how the wiki user interface will look like.',
   (
     ('sitename', u'Untitled Wiki',
@@ -398,17 +420,17 @@
     ('template_dirs', [], "list of directories with templates that will override theme and base templates."),
   )),
   # ==========================================================================
-  'editor': ('Editor related', None, (
+  'editor': ('Editor', None, (
     ('item_license', u'', 'if set, show the license item within the editor. [Unicode]'),
     #('edit_locking', 'warn 10', "Editor locking policy: `None`, `'warn <timeout in minutes>'`, or `'lock <timeout in minutes>'`"),
     ('edit_ticketing', True, None),
   )),
   # ==========================================================================
-  'paging': ('Paging related', None, (
+  'paging': ('Paging', None, (
     ('results_per_page', 50, "Number of results to be shown on a single page in pagination"),
   )),
   # ==========================================================================
-  'data': ('Data storage', None, (
+  'data': ('Data Storage', None, (
     ('data_dir', './data/', "Path to the data directory."),
     ('plugin_dirs', [], "Plugin directories."),
 
@@ -429,7 +451,7 @@
     ('destroy_index', False, "Destroy (empty) the index after using it."),
   )),
   # ==========================================================================
-  'items': ('Special item names', None, (
+  'items': ('Special Item Names', None, (
     ('item_root', u'Home', "Name of the root item (aka 'front page'). [Unicode]"),
 
     # the following regexes should match the complete name when used in free text
@@ -442,7 +464,7 @@
      'Item names exactly matching this regex are regarded as items containing group definitions [Unicode]'),
   )),
   # ==========================================================================
-  'user': ('User Preferences related', None, (
+  'user': ('User Preferences', None, (
     ('user_defaults',
       dict(
         name=[],
@@ -455,6 +477,7 @@
         scroll_page_after_edit=True,
         show_comments=False,
         want_trivial=False,
+        enc_password=u'',  # empty value == invalid hash
         disabled=False,
         bookmarks={},
         quicklinks=[],
@@ -531,7 +554,7 @@
 #
 #
 options = {
-    'acl': ('Access control lists',
+    'acl': ('Access Control Lists',
     'ACLs control who may do what.',
     (
       ('functions', u'',
@@ -551,7 +574,7 @@
       ('user_homepage', 'User/', 'All user homepages are stored below this namespace.'),
     )),
 
-    'user': ('Users / User settings', None, (
+    'user': ('User', None, (
       ('email_unique', True,
        "if True, check email addresses for uniqueness and don't accept duplicates."),
       ('email_verification', False,
@@ -562,7 +585,7 @@
       ('use_gravatar', False, "if True, gravatar.com will be used to find User's avatar")
     )),
 
-    'mail': ('Mail settings',
+    'mail': ('Mail',
         'These settings control outgoing and incoming email from and to the wiki.',
     (
       ('from', None, "Used as From: address for generated mail. [Unicode]"),
--- a/MoinMoin/constants/contenttypes.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/constants/contenttypes.py	Sun Feb 03 03:56:51 2013 +0100
@@ -17,6 +17,7 @@
 
 CONTENTTYPE_USER = u'application/x.moin.userprofile'
 CONTENTTYPE_DEFAULT = u'application/octet-stream'
+CONTENTTYPE_NONEXISTENT = u'application/x-nonexistent'
 
 
 GROUP_MARKUP_TEXT = 'markup text items'
--- a/MoinMoin/converter/_table.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/_table.py	Sun Feb 03 03:56:51 2013 +0100
@@ -9,11 +9,24 @@
 from __future__ import absolute_import, division
 
 from MoinMoin.util.tree import moin_page
+from emeraldtree import ElementTree as ET
+
+WORDBREAK_LEN = 30
 
 class TableMixin(object):
     """
     Mixin to support building a DOM table.
     """
+    def add_numeric_class(self, cell, table_cell):
+        """
+        Add numeric class attribute if cell is numeric.
+        """
+        try:
+            float(cell)
+            table_cell.attrib[moin_page('class')] = 'moin-integer'
+        except:
+            pass
+
     def build_dom_table(self, rows, head=None, cls=None):
         """
         Build a DOM table with data from <rows>.
@@ -24,8 +37,11 @@
         if head is not None:
             table_head = moin_page.table_header()
             table_row = moin_page.table_row()
-            for cell in head:
-                table_cell = moin_page.table_cell(children=[cell, ])
+            for idx, cell in enumerate(head):
+                table_cell = moin_page.table_cell(children=[cell, ],)
+                if rows:
+                    # add "align: right" to heading cell if cell in first data row is numeric
+                    self.add_numeric_class(rows[0][idx], table_cell)
                 table_row.append(table_cell)
             table_head.append(table_row)
             table.append(table_head)
@@ -33,7 +49,13 @@
         for row in rows:
             table_row = moin_page.table_row()
             for cell in row:
-                table_cell = moin_page.table_cell(children=[cell, ])
+                if isinstance(cell, ET.Node) and isinstance(cell[0], unicode) and \
+                    len(cell[0].split()) == 1 and len(cell[0]) > WORDBREAK_LEN:
+                    # avoid destroying table layout by applying special styling to cells with long file name hyperlinks
+                    table_cell = moin_page.table_cell(children=[cell, ], attrib={moin_page.class_: 'moin-wordbreak'})
+                else:
+                    table_cell = moin_page.table_cell(children=[cell, ],)
+                    self.add_numeric_class(cell, table_cell)
                 table_row.append(table_cell)
             table_body.append(table_row)
         table.append(table_body)
--- a/MoinMoin/converter/_tests/test_moinwiki_in.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/_tests/test_moinwiki_in.py	Sun Feb 03 03:56:51 2013 +0100
@@ -81,6 +81,10 @@
                 '<page><body><p>Text <emphasis>Emphasis</emphasis></p><p>Text</p></body></page>'),
             ("Text''''''Text''''",
                 '<page><body><p>TextText</p></body></page>'),
+            ("''italic '''strongitalic ''''' normal",
+                '<page><body><p><emphasis>italic <strong>strongitalic </strong></emphasis> normal</p></body></page>'),
+            ("'''strong '''''italic '''strongitalic''''' normal",
+                '<page><body><p><strong>strong </strong><emphasis>italic <strong>strongitalic</strong></emphasis> normal</p></body></page>'),
         ]
         for i in data:
             yield (self.do, ) + i
@@ -119,6 +123,10 @@
                 '<page><body><p><span font-size="120%">larger</span></p></body></page>'),
             ("--(strike through)--",
                 '<page><body><p><span text-decoration="line-through">strike through</span></p></body></page>'),
+            ("normal ~+big __underline__ big+~ normal",
+                '<page><body><p>normal <span font-size="120%">big <span text-decoration="underline">underline</span> big</span> normal</p></body></page>'),
+            ("/* normal __underline__ normal */",
+                '<page><body><p><span class="comment">normal <span text-decoration="underline">underline</span> normal</span></p></body></page>'),
             (u'&quot;',
                 '<page><body><p>"</p></body></page>'),
             (u'&#34;',
--- a/MoinMoin/converter/_util.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/_util.py	Sun Feb 03 03:56:51 2013 +0100
@@ -155,8 +155,9 @@
         if elem:
             self.top_append(elem)
 
-    def top_check(self, *names):
+    def top_check(self, *names, **kwargs):
         """
-        Checks if the name of the top of the stack matches the parameters.
+        Check if the top of the stack name and attrib matches the parameters.
         """
-        return self._list[-1].name in names
+        attrib = kwargs.get('attrib', {})
+        return self._list[-1].name in names and set(attrib.items()).issubset(self._list[-1].elem.attrib.items())
--- a/MoinMoin/converter/_wiki_macro.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/_wiki_macro.py	Sun Feb 03 03:56:51 2013 +0100
@@ -150,7 +150,7 @@
             type = Type(name)
         else:
             type = Type(type='x-moin', subtype='format', parameters={'name': name})
-        logging.debug("parser type: %r" % type)
+        logging.debug("parser type: %r" % (type, ))
 
         elem = moin_page.part(attrib={moin_page.content_type: type})
 
--- a/MoinMoin/converter/archive_in.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/archive_in.py	Sun Feb 03 03:56:51 2013 +0100
@@ -37,13 +37,14 @@
         return cls()
 
     def process_name(self, member_name):
+        name = unicode(member_name, 'utf-8')
         attrib = {
-            xlink.href: Iri(scheme='wiki', authority='', path='/'+self.item_name, query=u'do=get&member={0}'.format(member_name)),
+            xlink.href: Iri(scheme='wiki', authority='', path='/'+self.item_name, query=u'do=get&member={0}'.format(name)),
         }
-        return moin_page.a(attrib=attrib, children=[member_name, ])
+        return moin_page.a(attrib=attrib, children=[name, ])
 
     def process_datetime(self, dt):
-        return dt.isoformat()
+        return unicode(dt.isoformat(' '))
 
     def process_size(self, size):
         return unicode(size)
@@ -56,7 +57,7 @@
                          self.process_datetime(dt),
                          self.process_name(name),
                         ) for size, dt, name in contents]
-            table = self.build_dom_table(contents, head=[_("Size"), _("Date"), _("Name")], cls='zebra')
+            table = self.build_dom_table(contents, head=[_("Size"), _("Timestamp"), _("Name")], cls='zebra')
             body = moin_page.body(children=(table, ))
             return moin_page.page(children=(body, ))
         except ArchiveException as err:
@@ -87,11 +88,13 @@
             rows = []
             tf = tarfile.open(fileobj=fileobj, mode='r')
             for tinfo in tf.getmembers():
-                rows.append((
-                    tinfo.size,
-                    datetime.utcfromtimestamp(tinfo.mtime),
-                    tinfo.name,
-                ))
+                if tinfo.isfile():
+                    # display only normal files, not directories
+                    rows.append((
+                        tinfo.size,
+                        datetime.utcfromtimestamp(tinfo.mtime),
+                        tinfo.name,
+                    ))
             return rows
         except tarfile.TarError as err:
             raise ArchiveException(str(err))
@@ -106,11 +109,13 @@
             rows = []
             zf = zipfile.ZipFile(fileobj, mode='r')
             for zinfo in zf.filelist:
-                rows.append((
-                    zinfo.file_size,
-                    datetime(*zinfo.date_time), # y,m,d,h,m,s
-                    zinfo.filename,
-                ))
+                if not (zinfo.file_size == 0 and zinfo.filename.endswith('/')):
+                    # display only normal files, not directories
+                    rows.append((
+                        zinfo.file_size,
+                        datetime(*zinfo.date_time), # y,m,d,h,m,s
+                        zinfo.filename,
+                    ))
             return rows
         except (RuntimeError, zipfile.BadZipfile) as err:
             # RuntimeError is raised by zipfile stdlib module in case of
--- a/MoinMoin/converter/creole_in.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/creole_in.py	Sun Feb 03 03:56:51 2013 +0100
@@ -167,7 +167,7 @@
     """
 
     def block_nowiki_lines(self, iter_content):
-        "Unescaping generator for the lines in a nowiki block"
+        """Unescaping generator for the lines in a nowiki block"""
 
         for line in iter_content:
             match = self.nowiki_end_re.match(line)
@@ -178,7 +178,7 @@
             yield line
 
     def block_nowiki_repl(self, iter_content, stack, nowiki):
-        "Handles a complete nowiki block"
+        """Handles a complete nowiki block"""
 
         stack.clear()
 
--- a/MoinMoin/converter/docbook_in.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/docbook_in.py	Sun Feb 03 03:56:51 2013 +0100
@@ -232,7 +232,6 @@
         # XXX: Error handling could probably be better.
         except NameSpaceError as detail:
             return self.error(str(detail))
-        return result
 
     def error(self, message):
         """
--- a/MoinMoin/converter/docbook_out.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/docbook_out.py	Sun Feb 03 03:56:51 2013 +0100
@@ -17,6 +17,7 @@
 logging = log.getLogger(__name__)
 
 from MoinMoin.util.tree import html, moin_page, xlink, docbook, xml
+from MoinMoin.constants.contenttypes import CONTENTTYPE_NONEXISTENT
 
 
 class Converter(object):
@@ -353,7 +354,7 @@
         """
         href = element.get(xlink.href, None)
         attrib = {}
-        mimetype = Type(_type=element.get(moin_page.type_, 'application/x-nonexistent'))
+        mimetype = Type(_type=element.get(moin_page.type_, CONTENTTYPE_NONEXISTENT))
         if href:
             attrib[docbook.fileref] = href
             if Type('image/').issupertype(mimetype):
--- a/MoinMoin/converter/html_out.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/html_out.py	Sun Feb 03 03:56:51 2013 +0100
@@ -20,6 +20,7 @@
 from MoinMoin import wikiutil
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.tree import html, moin_page, xlink, xml, Name
+from MoinMoin.constants.contenttypes import CONTENTTYPE_NONEXISTENT
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
@@ -353,7 +354,7 @@
         # TODO: maybe IE8 would display transcluded external pages if we could do <object... type="text/html" ...>
         href = elem.get(xlink.href, None)
         attrib = {}
-        mimetype = Type(_type=elem.get(moin_page.type_, 'application/x-nonexistent'))
+        mimetype = Type(_type=elem.get(moin_page.type_, CONTENTTYPE_NONEXISTENT))
         # Get the object type
         obj_type = self.eval_object_type(mimetype, href)
 
--- a/MoinMoin/converter/mediawiki_in.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/mediawiki_in.py	Sun Feb 03 03:56:51 2013 +0100
@@ -161,7 +161,7 @@
     """
 
     def block_table_lines(self, iter_content):
-        "Unescaping generator for the lines in a table block"
+        """Unescaping generator for the lines in a table block"""
         for line in iter_content:
             match = self.table_end_re.match(line)
             if match:
--- a/MoinMoin/converter/moinwiki_in.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/moinwiki_in.py	Sun Feb 03 03:56:51 2013 +0100
@@ -249,7 +249,7 @@
     # Matches the possibly escaped end of a nowiki block
 
     def block_nowiki_lines(self, iter_content, marker_len):
-        "Unescaping generator for the lines in a nowiki block"
+        """Unescaping generator for the lines in a nowiki block"""
 
         for line in iter_content:
             match = self.nowiki_end_re.match(line)
@@ -533,10 +533,11 @@
                 else:
                     stack.push(moin_page.strong())
             elif stack.top_check('strong'):
-                if stack.top_check('strong'):
+                stack.pop()
+                if stack.top_check('emphasis'):
                     stack.pop()
                 else:
-                    stack.push(moin_page.strong())
+                    stack.push(moin_page.emphasis())
             else:
                 if len(emphstrong_follow) == 3:
                     stack.push(moin_page.emphasis())
@@ -654,11 +655,11 @@
     """
 
     def inline_underline_repl(self, stack, underline):
-        if not stack.top_check('span'):
-            attrib = {moin_page.text_decoration: 'underline'}
+        attrib = {moin_page.text_decoration: 'underline'}
+        if stack.top_check('span', attrib=attrib):
+            stack.pop()
+        else:
             stack.push(moin_page.span(attrib=attrib))
-        else:
-            stack.pop()
 
     inline_link = r"""
         (?P<link>
--- a/MoinMoin/converter/moinwiki_out.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/moinwiki_out.py	Sun Feb 03 03:56:51 2013 +0100
@@ -21,10 +21,10 @@
 
 
 class Moinwiki(object):
-    '''
+    """
     Moinwiki syntax elements
     It's dummy
-    '''
+    """
     h = u'='
     a_open = u'[['
     a_separator = u'|'
--- a/MoinMoin/converter/nonexistent_in.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/nonexistent_in.py	Sun Feb 03 03:56:51 2013 +0100
@@ -13,6 +13,7 @@
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.tree import moin_page, xlink
+from MoinMoin.constants.contenttypes import CONTENTTYPE_NONEXISTENT
 
 
 class Converter(object):
@@ -34,4 +35,4 @@
 
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
-default_registry.register(Converter._factory, Type('application/x-nonexistent'), type_moin_document)
+default_registry.register(Converter._factory, Type(CONTENTTYPE_NONEXISTENT), type_moin_document)
--- a/MoinMoin/converter/rst_out.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/converter/rst_out.py	Sun Feb 03 03:56:51 2013 +0100
@@ -192,7 +192,7 @@
                 line = [u'+']
                 for col in range(len(cols)):
                     if self.table[row][col][1] > 1:
-                        line.append(' '*cols[col])
+                        line.append(u' '*cols[col])
                     elif row == self.header_count - 1:
                         line.append(u'='*cols[col])
                     else:
--- a/MoinMoin/items/__init__.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/items/__init__.py	Sun Feb 03 03:56:51 2013 +0100
@@ -54,7 +54,10 @@
     CONTENTTYPE, SIZE, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT,
     HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID
     )
-from MoinMoin.constants.contenttypes import charset
+from MoinMoin.constants.contenttypes import charset, CONTENTTYPE_NONEXISTENT
+from MoinMoin.constants.itemtypes import (
+    ITEMTYPE_NONEXISTENT, ITEMTYPE_USERPROFILE, ITEMTYPE_DEFAULT,
+    )
 
 from .content import content_registry, Content, NonExistentContent, Draw
 
@@ -102,8 +105,8 @@
     def __init__(self, item, itemtype=None, contenttype=None):
         self.item = item
         self.meta = {
-            ITEMTYPE: itemtype or u'nonexistent',
-            CONTENTTYPE: contenttype or u'application/x-nonexistent'
+            ITEMTYPE: itemtype or ITEMTYPE_NONEXISTENT,
+            CONTENTTYPE: contenttype or CONTENTTYPE_NONEXISTENT
         }
         self.data = StringIO('')
         self.revid = None
@@ -445,6 +448,7 @@
                                              action=unicode(action),
                                              contenttype_current=contenttype_current,
                                              contenttype_guessed=contenttype_guessed,
+                                             return_rev=True,
                                              )
         item_modified.send(app._get_current_object(), item_name=name)
         return newrev.revid, newrev.meta[SIZE]
@@ -594,7 +598,7 @@
     """
     A "conventional" wiki item.
     """
-    itemtype = u'default'
+    itemtype = ITEMTYPE_DEFAULT
     display_name = L_('Default')
     description = L_('Wiki item')
     order = -10
@@ -686,7 +690,7 @@
     Currently userprofile is implemented as a contenttype. This is a stub of an
     itemtype implementation of userprofile.
     """
-    itemtype = u'userprofile'
+    itemtype = ITEMTYPE_USERPROFILE
     display_name = L_('User profile')
     description = L_('User profile item (not implemented yet!)')
 
@@ -697,7 +701,7 @@
     A dummy Item for nonexistent items (when modifying, a nonexistent item with
     undetermined itemtype)
     """
-    itemtype = u'nonexistent'
+    itemtype = ITEMTYPE_NONEXISTENT
     shown = False
 
     def _convert(self, doc):
--- a/MoinMoin/items/content.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/items/content.py	Sun Feb 03 03:56:51 2013 +0100
@@ -63,7 +63,7 @@
 from MoinMoin.forms import File
 from MoinMoin.constants.contenttypes import (
     GROUP_MARKUP_TEXT, GROUP_OTHER_TEXT, GROUP_IMAGE, GROUP_AUDIO, GROUP_VIDEO,
-    GROUP_DRAWING, GROUP_OTHER,
+    GROUP_DRAWING, GROUP_OTHER, CONTENTTYPE_NONEXISTENT,
     )
 from MoinMoin.constants.keys import (
     NAME, NAME_EXACT, WIKINAME, CONTENTTYPE, SIZE, TAGS, HASH_ALGORITHM
@@ -82,11 +82,10 @@
 
         def __lt__(self, other):
             if isinstance(other, self.__class__):
-                if self.content_type != other.content_type:
-                    return other.content_type.issupertype(self.content_type)
-                if self.priority != other.priority:
-                    return self.priority < other.priority
-                return False
+                # Within the registry, content_type is sorted in descending
+                # order (more specific first) while priority is in ascending
+                # order (smaller first).
+                return (other.content_type, self.priority) < (self.content_type, other.priority)
             return NotImplemented
 
     def __init__(self, group_names):
@@ -232,15 +231,27 @@
         return doc
 
     def _render_data(self):
-        from MoinMoin.converter import default_registry as reg
-        # TODO: Real output format
-        doc = self.internal_representation()
-        doc = self._expand_document(doc)
-        flaskg.clock.start('conv_dom_html')
-        html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page'))
-        doc = html_conv(doc)
-        flaskg.clock.stop('conv_dom_html')
-        rendered_data = conv_serialize(doc, {html.namespace: ''})
+        try:
+            from MoinMoin.converter import default_registry as reg
+            # TODO: Real output format
+            doc = self.internal_representation()
+            doc = self._expand_document(doc)
+            flaskg.clock.start('conv_dom_html')
+            html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page'))
+            doc = html_conv(doc)
+            flaskg.clock.stop('conv_dom_html')
+            rendered_data = conv_serialize(doc, {html.namespace: ''})
+        except Exception:
+            # we really want to make sure that invalid data or a malfunctioning
+            # converter does not crash the item view (otherwise a user might
+            # not be able to fix it from the UI).
+            import time, uuid
+            error_id = uuid.uuid4()
+            logging.exception("An exception happened in _render_data (error_id = %s ):" % error_id)
+            rendered_data = render_template('crash.html',
+                                            server_time=time.strftime("%Y-%m-%d %H:%M:%S %Z"),
+                                            url=request.url,
+                                            error_id=error_id)
         return rendered_data
 
     def _render_data_xml(self):
@@ -269,7 +280,7 @@
 @register
 class NonExistentContent(Content):
     """Dummy Content to use with NonExistent."""
-    contenttype = 'application/x-nonexistent'
+    contenttype = CONTENTTYPE_NONEXISTENT
     group = None
 
     def do_get(self, force_attachment=False, mimetype=None):
@@ -348,6 +359,9 @@
             mt = MimeType(filename=filename)
             content_length = None
             file_to_send = self.get_member(member)
+            # force attachment download, so it uses attachment_filename
+            # otherwise it will use the itemname from the URL for saving
+            force_attachment = True
         else: # content = item revision
             rev = self.rev
             filename = rev.item.name
@@ -1011,21 +1025,45 @@
         # Set the workaround flag respected in modify.html
         is_draw = True
 
-    def handle_post():
+    def handle_post(self):
         raise NotImplementedError
 
 
-@register
-class TWikiDraw(Draw):
-    """
-    drawings by TWikiDraw applet. It creates three files which are stored as tar file.
+class DrawPNGMap(Draw):
     """
-    contenttype = 'application/x-twikidraw'
-    display_name = 'TDRAW'
+    Base class for drawings that have a png with click map
+    """
+    def _read_map(self):
+        mapfile = self.get_member('drawing.map')
+        try:
+            image_map = mapfile.read()
+            mapfile.close()
+        except (IOError, OSError):
+            image_map = ''
+        return image_map
 
-    class ModifyForm(Draw.ModifyForm):
-        template = "modify_twikidraw.html"
-        help = ""
+    def _transform_map(self, image_map, title):
+        raise NotImplementedError
+
+    def _render_data(self):
+        # TODO: this could be a converter -> dom, then transcluding this kind
+        # of items and also rendering them with the code in base class could work
+        png_url = url_for('frontend.get_item', item_name=self.name, member='drawing.png', rev=self.rev.revid)
+        title = _('Edit drawing %(filename)s (opens in new window)', filename=self.name)
+        image_map = self._read_map()
+        if image_map:
+            mapid, image_map = self._transform_map(image_map, title)
+            title = _('Clickable drawing: %(filename)s', filename=self.name)
+            return Markup(image_map + u'<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid))
+        else:
+            return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, title))
+
+
+class DrawAWDTWDBase(DrawPNGMap):
+    """
+    Shared code between TWikiDraw and AnyWikiDraw
+    """
+    _expected_members = set()
 
     def handle_post(self):
         # called from modify UI/POST
@@ -1033,10 +1071,9 @@
         filename = request.form['filename']
         basepath, basename = os.path.split(filename)
         basename, ext = os.path.splitext(basename)
-
         filecontent = file_upload.stream
         content_length = None
-        if ext == '.draw': # TWikiDraw POSTs this first
+        if ext in ['.svg', '.draw', ]:  # handle AWD (svg) and TWD (draw)
             filecontent = filecontent.read() # read file completely into memory
             filecontent = filecontent.replace("\r", "")
         elif ext == '.map':
@@ -1047,45 +1084,42 @@
             # XXX gives -1 for wsgiref, gives 0 for werkzeug :(
             # If this is fixed, we could use the file obj, without reading it into memory completely:
             filecontent = filecontent.read()
-
         self.put_member('drawing' + ext, filecontent, content_length,
-                        expected_members=set(['drawing.draw', 'drawing.map', 'drawing.png']))
-
-    def _render_data(self):
-        # TODO: this could be a converter -> dom, then transcluding this kind
-        # of items and also rendering them with the code in base class could work
-        item_name = self.name
-        drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.draw', rev=self.rev.revid)
-        png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid)
-        title = _('Edit drawing %(filename)s (opens in new window)', filename=item_name)
-
-        mapfile = self.get_member('drawing.map')
-        try:
-            image_map = mapfile.read()
-            mapfile.close()
-        except (IOError, OSError):
-            image_map = ''
-        if image_map:
-            # we have a image map. inline it and add a map ref to the img tag
-            mapid = 'ImageMapOf' + item_name
-            image_map = image_map.replace('%MAPNAME%', mapid)
-            # add alt and title tags to areas
-            image_map = re.sub(r'href\s*=\s*"((?!%TWIKIDRAW%).+?)"', r'href="\1" alt="\1" title="\1"', image_map)
-            image_map = image_map.replace('%TWIKIDRAW%"', '{0}" alt="{1}" title="{2}"'.format((drawing_url, title, title)))
-            title = _('Clickable drawing: %(filename)s', filename=item_name)
-
-            return Markup(image_map + u'<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid))
-        else:
-            return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, title))
+                        expected_members=self._expected_members)
 
 
 @register
-class AnyWikiDraw(Draw):
+class TWikiDraw(DrawAWDTWDBase):
+    """
+    drawings by TWikiDraw applet. It creates three files which are stored as tar file.
+    """
+    contenttype = 'application/x-twikidraw'
+    display_name = 'TDRAW'
+    _expected_members = set(['drawing.draw', 'drawing.map', 'drawing.png'])
+
+    class ModifyForm(Draw.ModifyForm):
+        template = "modify_twikidraw.html"
+        help = ""
+
+    def _transform_map(self, image_map, title):
+        mapid = 'ImageMapOf' + self.name  # TODO: make it unique
+        image_map = image_map.replace('%MAPNAME%', mapid)
+        # add alt and title tags to areas
+        image_map = re.sub(r'href\s*=\s*"((?!%TWIKIDRAW%).+?)"',
+                           r'href="\1" alt="\1" title="\1"', image_map)
+        drawing_url = url_for('frontend.get_item', item_name=self.name, member='drawing.draw', rev=self.rev.revid)
+        image_map = image_map.replace('%TWIKIDRAW%"', '{0}" alt="{1}" title="{2}"'.format(drawing_url, title, title))
+        return mapid, image_map
+
+
+@register
+class AnyWikiDraw(DrawAWDTWDBase):
     """
     drawings by AnyWikiDraw applet. It creates three files which are stored as tar file.
     """
     contenttype = 'application/x-anywikidraw'
     display_name = 'ADRAW'
+    _expected_members = set(['drawing.svg', 'drawing.map', 'drawing.png'])
 
     class ModifyForm(Draw.ModifyForm):
         template = "modify_anywikidraw.html"
@@ -1098,55 +1132,14 @@
                 drawing_exists = False
             self.drawing_exists = drawing_exists
 
-    def handle_post(self):
-        # called from modify UI/POST
-        file_upload = request.files.get('filepath')
-        filename = request.form['filename']
-        basepath, basename = os.path.split(filename)
-        basename, ext = os.path.splitext(basename)
-        filecontent = file_upload.stream
-        content_length = None
-        if ext == '.svg':
-            filecontent = filecontent.read() # read file completely into memory
-            filecontent = filecontent.replace("\r", "")
-        elif ext == '.map':
-            filecontent = filecontent.read() # read file completely into memory
-            filecontent = filecontent.strip()
-        elif ext == '.png':
-            #content_length = file_upload.content_length
-            # XXX gives -1 for wsgiref, gives 0 for werkzeug :(
-            # If this is fixed, we could use the file obj, without reading it into memory completely:
-            filecontent = filecontent.read()
-        self.put_member('drawing' + ext, filecontent, content_length,
-                        expected_members=set(['drawing.svg', 'drawing.map', 'drawing.png']))
-
-    def _render_data(self):
-        # TODO: this could be a converter -> dom, then transcluding this kind
-        # of items and also rendering them with the code in base class could work
-        item_name = self.name
-        drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg', rev=self.rev.revid)
-        png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid)
-        title = _('Edit drawing %(filename)s (opens in new window)', filename=self.name)
-
-        mapfile = self.get_member('drawing.map')
-        try:
-            image_map = mapfile.read()
-            mapfile.close()
-        except (IOError, OSError):
-            image_map = ''
-        if image_map:
-            # ToDo mapid must become uniq
-            # we have a image map. inline it and add a map ref to the img tag
-            # we have also to set a unique ID
-            mapid = 'ImageMapOf' + self.name
-            image_map = image_map.replace(u'id="drawing.svg"', '')
-            image_map = image_map.replace(u'name="drawing.svg"', u'name="{0}"'.format(mapid))
-            # unxml, because 4.01 concrete will not validate />
-            image_map = image_map.replace(u'/>', u'>')
-            title = _('Clickable drawing: %(filename)s', filename=self.name)
-            return Markup(image_map + u'<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid))
-        else:
-            return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, title))
+    def _transform_map(self, image_map, title):
+        #drawing_url = url_for('frontend.get_item', item_name=self.name, member='drawing.svg', rev=self.rev.revid)
+        mapid = 'ImageMapOf' + self.name  # TODO: make it unique
+        image_map = image_map.replace(u'id="drawing.svg"', '')
+        image_map = image_map.replace(u'name="drawing.svg"', u'name="{0}"'.format(mapid))
+        # unxml, because 4.01 concrete will not validate />
+        image_map = image_map.replace(u'/>', u'>')
+        return mapid, image_map
 
 
 @register
@@ -1163,7 +1156,6 @@
         # called from modify UI/POST
         png_upload = request.values.get('png_data')
         svg_upload = request.values.get('filepath')
-        filename = request.form['filename']
         png_content = png_upload.decode('base_64')
         png_content = base64.urlsafe_b64decode(png_content.split(',')[1])
         svg_content = svg_upload.decode('base_64')
@@ -1176,7 +1168,6 @@
     def _render_data(self):
         # TODO: this could be a converter -> dom, then transcluding this kind
         # of items and also rendering them with the code in base class could work
-        item_name = self.name
-        drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg', rev=self.rev.revid)
-        png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid)
+        drawing_url = url_for('frontend.get_item', item_name=self.name, member='drawing.svg', rev=self.rev.revid)
+        png_url = url_for('frontend.get_item', item_name=self.name, member='drawing.png', rev=self.rev.revid)
         return Markup(u'<img src="{0}" alt="{1}" />'.format(png_url, drawing_url))
--- a/MoinMoin/mail/sendmail.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/mail/sendmail.py	Sun Feb 03 03:56:51 2013 +0100
@@ -85,6 +85,8 @@
     from email.Utils import formatdate, make_msgid
 
     cfg = app.cfg
+    if not cfg.mail_enabled:
+        return (0, _("Contact administrator: cannot send password recovery e-mail because mail configuration is incomplete."))
     mail_from = mail_from or cfg.mail_from
 
     logging.debug("send mail, from: {0!r}, subj: {1!r}".format(mail_from, subject))
--- a/MoinMoin/script/__init__.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/script/__init__.py	Sun Feb 03 03:56:51 2013 +0100
@@ -34,20 +34,20 @@
     manager.add_command("save", serialization.Serialize())
     manager.add_command("load", serialization.Deserialize())
     from MoinMoin.script.account.create import Create_User
-    manager.add_command("account_create", Create_User())
+    manager.add_command("account-create", Create_User())
     from MoinMoin.script.account.disable import Disable_User
-    manager.add_command("account_disable", Disable_User())
+    manager.add_command("account-disable", Disable_User())
     from MoinMoin.script.account.resetpw import Set_Password
-    manager.add_command("account_password", Set_Password())
+    manager.add_command("account-password", Set_Password())
     from MoinMoin.script.maint.reduce_revisions import Reduce_Revisions
-    manager.add_command("maint_reduce_revisions", Reduce_Revisions())
+    manager.add_command("maint-reduce-revisions", Reduce_Revisions())
     from MoinMoin.script.maint.set_meta import Set_Meta
-    manager.add_command("maint_set_meta", Set_Meta())
+    manager.add_command("maint-set-meta", Set_Meta())
     from MoinMoin.script.maint import modify_item
     manager.add_command("item-get", modify_item.GetItem())
     manager.add_command("item-put", modify_item.PutItem())
     from MoinMoin.script.maint.modified_systemitems import Modified_SystemItems
-    manager.add_command("maint_modified_systemitems", Modified_SystemItems())
+    manager.add_command("maint-modified-systemitems", Modified_SystemItems())
     from MoinMoin.script.migration.moin19.import19 import ImportMoin19
     manager.add_command("import19", ImportMoin19())
 
--- a/MoinMoin/script/account/resetpw.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/script/account/resetpw.py	Sun Feb 03 03:56:51 2013 +0100
@@ -1,4 +1,4 @@
-# Copyright: 2006 MoinMoin:ThomasWaldmann
+# Copyright: 2006-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2008 MoinMoin:JohannesBerg
 # Copyright: 2011 MoinMoin:ReimarBauer
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
@@ -12,9 +12,32 @@
 from flask import g as flaskg
 from flask.ext.script import Command, Option
 
+from MoinMoin.constants.keys import ITEMID, NAME, NAME_EXACT, EMAIL
 from MoinMoin import user
 from MoinMoin.app import before_wiki
-from MoinMoin.util import crypto
+
+
+class Fault(Exception):
+    """something went wrong"""
+
+class NoSuchUser(Fault):
+    """raised if no such user exists"""
+
+class MailFailed(Fault):
+    """raised if e-mail sending failed"""
+
+
+def set_password(uid, password, notify=False):
+    u = user.User(uid)
+    if u and u.exists():
+        u.set_password(password)
+        u.save()
+        if notify and not u.disabled and u.email:
+            mailok, msg = u.mail_password_recovery()
+            if not mailok:
+                raise MailFailed(msg)
+    else:
+        raise NoSuchUser('User does not exist (name: %r id: %r)!' % (u.name, u.id))
 
 
 class Set_Password(Command):
@@ -24,12 +47,18 @@
                help='Set password for the user with user name NAME.'),
         Option('--uid', '-u', required=False, dest='uid', type=unicode,
                help='Set password for the user with user id UID.'),
-        Option('--password', '-p', required=True, dest='password', type=unicode,
+        Option('--password', '-p', required=False, dest='password', type=unicode,
                help='New password for this account.'),
+        Option('--all-users', '-a', required=False, dest='all_users', action='store_true', default=False,
+            help='Reset password for ALL users.'),
+        Option('--notify', '-N', required=False, dest='notify', action='store_true', default=False,
+            help='Notify user(s), send them an E-Mail with a password reset link.'),
+        Option('--verbose', '-v', required=False, dest='verbose', action='store_true', default=False,
+            help='Verbose operation'),
     )
 
-    def run(self, name, uid, password):
-        flags_given = name or uid
+    def run(self, name, uid, password, all_users, notify, verbose):
+        flags_given = name or uid or all_users
         if not flags_given:
             print 'incorrect number of arguments'
             import sys
@@ -37,14 +66,23 @@
 
         before_wiki()
         if uid:
-            u = user.User(uid)
+            query = {ITEMID: uid}
         elif name:
-            u = user.User(auth_username=name)
+            query = {NAME_EXACT: name}
+        elif all_users:
+            query = {}
 
-        if not u.exists():
-            print 'This user "{0!r}" does not exists!'.format(u.name)
-            return
-
-        u.enc_password = crypto.crypt_password(password)
-        u.save()
-        print 'Password set.'
+        # sorting the list so we have some specific, reproducable order
+        uids_metas = sorted([(rev.meta[ITEMID], rev.meta) for rev in user.search_users(**query)])
+        total = len(uids_metas)
+        for nr, (uid, meta) in enumerate(uids_metas, start=1):
+            name = meta[NAME]
+            email = meta[EMAIL]
+            try:
+                set_password(uid, password, notify=notify)
+            except Fault, err:
+                status = "FAILURE: [%s]" % str(err)
+            else:
+                status = "SUCCESS"
+            if verbose:
+                print "uid %s, name %s, email %s (%05d / %05d) %s" % (uid, name, email, nr, total, status)
--- a/MoinMoin/script/migration/moin19/import19.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/script/migration/moin19/import19.py	Sun Feb 03 03:56:51 2013 +0100
@@ -228,7 +228,8 @@
                                      # if we have an entry there
                    }
             try:
-                previous_meta = PageRevision(item, revno-1)._fs_meta
+                revpath = os.path.join(item.path, 'revisions', '{0:08d}'.format(revno-1))
+                previous_meta = PageRevision(item, revno-1, revpath).meta
                 # if this page revision is deleted, we have no on-page metadata.
                 # but some metadata is required, thus we have to copy it from the
                 # (non-deleted) revision revno-1:
@@ -539,7 +540,7 @@
                 'editor_default', # not used any more
                 'editor_ui', # not used any more
                 'external_target', # ancient, not used any more
-                'passwd', # ancient, not used any more (use enc_passwd)
+                'passwd', # ancient, not used any more (use enc_password)
                 'show_emoticons', # ancient, not used any more
                 'show_fancy_diff', # kind of diff display now depends on mimetype
                 'show_fancy_links', # not used any more (now link rendering depends on theme)
@@ -571,6 +572,17 @@
             if key in metadata and metadata[key] in [u'', tuple(), {}, [], ]:
                 del metadata[key]
 
+        # moin2 only supports passlib generated hashes, drop everything else
+        # (users need to do pw recovery in case they are affected)
+        pw = metadata.get('enc_password')
+        if pw is not None:
+            if pw.startswith('{PASSLIB}'):
+                # take it, but strip the prefix as moin2 does not use that any more
+                metadata['enc_password'] = pw[len('{PASSLIB}'):]
+            else:
+                # drop old, unsupported (and also more or less unsafe) hashing scheme
+                del metadata['enc_password']
+
         # TODO quicklinks and subscribed_items - check for non-interwiki elements and convert them to interwiki
 
         return metadata
--- a/MoinMoin/search/__init__.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/search/__init__.py	Sun Feb 03 03:56:51 2013 +0100
@@ -26,7 +26,7 @@
 
 class SearchForm(Form):
     q = Search
-    history = InlineCheckbox.using(label=L_('search also in non-current revisions'))
+    history = InlineCheckbox.using(label=L_('search all revisions'))
     submit = Submit.using(default=L_('Search'))
 
     validators = [ValidSearch()]
--- a/MoinMoin/security/_tests/test_textcha.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/security/_tests/test_textcha.py	Sun Feb 03 03:56:51 2013 +0100
@@ -103,7 +103,7 @@
         cfg.secrets.pop('security/textcha')
         flaskg.user.profile[LOCALE] = None
 
-    class Element:
+    class Element(object):
         def __init__(self):
             self.parent = None
             self.value = 'Good Answer'
--- a/MoinMoin/storage/__init__.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/__init__.py	Sun Feb 03 03:56:51 2013 +0100
@@ -33,7 +33,7 @@
     """
     backend_name_uri = uri.split(':', 1)
     if len(backend_name_uri) != 2:
-        raise ValueError("malformed backend uri: {0}".format(backend_uri))
+        raise ValueError("malformed backend uri: {0}".format(uri))
     backend_name, backend_uri = backend_name_uri
     module = __import__(BACKENDS_PACKAGE + '.' + backend_name, globals(), locals(), ['MutableBackend', ])
     return module.MutableBackend.from_uri(backend_uri)
--- a/MoinMoin/storage/middleware/_tests/test_indexing.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/middleware/_tests/test_indexing.py	Sun Feb 03 03:56:51 2013 +0100
@@ -53,7 +53,8 @@
         item_name = u'foo'
         data = 'bar'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=[item_name, ]), StringIO(data))
+        rev = item.store_revision(dict(name=[item_name, ]), StringIO(data),
+                                  return_rev=True)
         revid = rev.revid
         # check if we have the revision now:
         item = self.imw[item_name]
@@ -69,7 +70,8 @@
         data = 'bar'
         newdata = 'baz'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=[item_name, ], comment=u'spam'), StringIO(data))
+        rev = item.store_revision(dict(name=[item_name, ], comment=u'spam'), StringIO(data),
+                                  return_rev=True)
         revid = rev.revid
         # clear revision:
         item.store_revision(dict(name=[item_name, ], revid=revid, comment=u'no spam'), StringIO(newdata), overwrite=True)
@@ -87,13 +89,13 @@
         item_name = u'foo'
         item = self.imw[item_name]
         rev = item.store_revision(dict(name=[item_name, ], mtime=1),
-                                  StringIO('bar'), trusted=True)
+                                  StringIO('bar'), trusted=True, return_rev=True)
         revid0 = rev.revid
         rev = item.store_revision(dict(name=[item_name, ], mtime=2),
-                                  StringIO('baz'), trusted=True)
+                                  StringIO('baz'), trusted=True, return_rev=True)
         revid1 = rev.revid
         rev = item.store_revision(dict(name=[item_name, ], mtime=3),
-                                  StringIO('...'), trusted=True)
+                                  StringIO('...'), trusted=True, return_rev=True)
         revid2 = rev.revid
         print "revids:", revid0, revid1, revid2
         # destroy a non-current revision:
@@ -129,10 +131,10 @@
         item_name = u'foo'
         item = self.imw[item_name]
         rev = item.store_revision(dict(name=[item_name, ], mtime=1),
-                                  StringIO('bar'), trusted=True)
+                                  StringIO('bar'), trusted=True, return_rev=True)
         revids.append(rev.revid)
         rev = item.store_revision(dict(name=[item_name, ], mtime=2),
-                                  StringIO('baz'), trusted=True)
+                                  StringIO('baz'), trusted=True, return_rev=True)
         revids.append(rev.revid)
         # destroy item:
         item.destroy_all_revisions()
@@ -160,7 +162,8 @@
         item_name = u'bar'
         item = self.imw[item_name]
         item.store_revision(dict(name=[item_name, ]), StringIO('1st'))
-        expected_rev = item.store_revision(dict(name=[item_name, ]), StringIO('2nd'))
+        expected_rev = item.store_revision(dict(name=[item_name, ]), StringIO('2nd'),
+                                           return_rev=True)
         revs = list(self.imw.documents(name=item_name))
         assert len(revs) == 1  # there is only 1 latest revision
         assert expected_rev.revid == revs[0].revid  # it is really the latest one
@@ -169,7 +172,7 @@
         item_name = u'foo'
         data = 'bar'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=[item_name, ]), StringIO(data))
+        rev = item.store_revision(dict(name=[item_name, ]), StringIO(data), return_rev=True)
         print repr(rev.meta)
         assert rev.name == item_name
         assert rev.meta[SIZE] == len(data)
@@ -181,9 +184,9 @@
     def test_documents(self):
         item_name = u'foo'
         item = self.imw[item_name]
-        rev1 = item.store_revision(dict(name=[item_name, ]), StringIO('x'))
-        rev2 = item.store_revision(dict(name=[item_name, ]), StringIO('xx'))
-        rev3 = item.store_revision(dict(name=[item_name, ]), StringIO('xxx'))
+        rev1 = item.store_revision(dict(name=[item_name, ]), StringIO('x'), return_rev=True)
+        rev2 = item.store_revision(dict(name=[item_name, ]), StringIO('xx'), return_rev=True)
+        rev3 = item.store_revision(dict(name=[item_name, ]), StringIO('xxx'), return_rev=True)
         rev = self.imw.document(idx_name=ALL_REVS, size=2)
         assert rev
         assert rev.revid == rev2.revid
@@ -197,14 +200,15 @@
         item_name = u'foo'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=[item_name, ], mtime=1),
-                                StringIO('does not count, different name'), trusted=True)
+                                StringIO('does not count, different name'),
+                                trusted=True, return_rev=True)
         expected_latest_revids.append(r.revid)
         item_name = u'bar'
         item = self.imw[item_name]
         item.store_revision(dict(name=[item_name, ], mtime=1),
                             StringIO('1st'), trusted=True)
         r = item.store_revision(dict(name=[item_name, ], mtime=2),
-                                StringIO('2nd'), trusted=True)
+                                StringIO('2nd'), trusted=True, return_rev=True)
         expected_latest_revids.append(r.revid)
 
         # now we remember the index contents built that way:
@@ -246,24 +250,28 @@
         item_name = u'updated'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=[item_name, ], mtime=1),
-                                StringIO('updated 1st'), trusted=True)
+                                StringIO('updated 1st'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         # we update this item below, so we don't add it to expected_latest_revids
         item_name = u'destroyed'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=[item_name, ], mtime=1),
-                                StringIO('destroyed 1st'), trusted=True)
+                                StringIO('destroyed 1st'),
+                                trusted=True, return_rev=True)
         destroy_revid = r.revid
         # we destroy this item below, so we don't add it to expected_all_revids
         # we destroy this item below, so we don't add it to expected_latest_revids
         item_name = u'stayssame'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=[item_name, ], mtime=1),
-                                StringIO('stayssame 1st'), trusted=True)
+                                StringIO('stayssame 1st'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         # we update this item below, so we don't add it to expected_latest_revids
         r = item.store_revision(dict(name=[item_name, ], mtime=2),
-                                StringIO('stayssame 2nd'), trusted=True)
+                                StringIO('stayssame 2nd'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         expected_latest_revids.append(r.revid)
 
@@ -279,14 +287,16 @@
         item_name = u'updated'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=[item_name, ], mtime=2),
-                                StringIO('updated 2nd'), trusted=True)
+                                StringIO('updated 2nd'), trusted=True,
+                                return_rev=True)
         expected_all_revids.append(r.revid)
         expected_latest_revids.append(r.revid)
         missing_revids.append(r.revid)
         item_name = u'added'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=[item_name, ], mtime=1),
-                                StringIO('added 1st'), trusted=True)
+                                StringIO('added 1st'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         expected_latest_revids.append(r.revid)
         missing_revids.append(r.revid)
@@ -337,7 +347,7 @@
         data = 'some test content'
         item = self.imw[item_name]
         data_file = StringIO(data)
-        with item.store_revision(meta, data_file) as rev:
+        with item.store_revision(meta, data_file, return_rev=True) as rev:
             assert rev.data.read() == data
             revid = rev.revid
         with pytest.raises(ValueError):
@@ -355,7 +365,7 @@
         data = 'some test content\n'
         item = self.imw[item_name]
         data_file = StringIO(data)
-        with item.store_revision(meta, data_file) as rev:
+        with item.store_revision(meta, data_file, return_rev=True) as rev:
             expected_revid = rev.revid
         doc = self.imw._document(content=u'test')
         assert doc is not None
@@ -365,10 +375,12 @@
     def test_namespaces(self):
         item_name_n = u'normal'
         item = self.imw[item_name_n]
-        rev_n = item.store_revision(dict(name=[item_name_n, ], contenttype=u'text/plain'), StringIO(str(item_name_n)))
+        rev_n = item.store_revision(dict(name=[item_name_n, ], contenttype=u'text/plain'),
+                                    StringIO(str(item_name_n)), return_rev=True)
         item_name_u = u'userprofiles:userprofile'
         item = self.imw[item_name_u]
-        rev_u = item.store_revision(dict(name=[item_name_u, ], contenttype=u'text/plain'), StringIO(str(item_name_u)))
+        rev_u = item.store_revision(dict(name=[item_name_u, ], contenttype=u'text/plain'),
+                                    StringIO(str(item_name_u)), return_rev=True)
         item = self.imw[item_name_n]
         rev_n = item.get_revision(rev_n.revid)
         assert rev_n.meta[NAMESPACE] == u''
@@ -393,7 +405,8 @@
     def test_documents(self):
         item_name = u'public'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=[item_name, ], acl=u'joe:read'), StringIO('public content'))
+        r = item.store_revision(dict(name=[item_name, ], acl=u'joe:read'),
+                                StringIO('public content'), return_rev=True)
         revid_public = r.revid
         revids = [rev.revid for rev in self.imw.documents()
                   if rev.name != u'joe'] # the user profile is a revision in the backend
@@ -402,7 +415,8 @@
     def test_getitem(self):
         item_name = u'public'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=[item_name, ], acl=u'joe:read'), StringIO('public content'))
+        r = item.store_revision(dict(name=[item_name, ], acl=u'joe:read'),
+                                StringIO('public content'), return_rev=True)
         revid_public = r.revid
         # now testing:
         item_name = u'public'
--- a/MoinMoin/storage/middleware/_tests/test_protecting.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/middleware/_tests/test_protecting.py	Sun Feb 03 03:56:51 2013 +0100
@@ -63,8 +63,8 @@
         revids = []
         for item_name, acl, content in items:
             item = self.imw[item_name]
-            r = item.store_revision(dict(name=[item_name, ], acl=acl, contenttype=u'text/plain'),
-                                    StringIO(content))
+            r = item.store_revision(dict(name=[item_name, ], acl=acl),  # need contenttype=u'text/plain' in dict?
+                                    StringIO(content), return_rev=True)
             revids.append(r.revid)
         return revids
 
--- a/MoinMoin/storage/middleware/indexing.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/middleware/indexing.py	Sun Feb 03 03:56:51 2013 +0100
@@ -936,6 +936,7 @@
                        contenttype_current=None,
                        contenttype_guessed=None,
                        acl_parent=None,
+                       return_rev=False,
                        ):
         """
         Store a revision into the backend, write metadata and data to it.
@@ -947,7 +948,8 @@
         :type meta: dict
         :type data: open file (file must be closed by caller)
         :param overwrite: if True, allow overwriting of existing revs.
-        :returns: a Revision instance of the just created revision
+        :param return_rev: if True, return a Revision instance of the just created revision
+        :returns: a Revision instance or None
         """
         if remote_addr is None:
             try:
@@ -1010,7 +1012,8 @@
         self.indexer.index_revision(meta, content, backend_name)
         if not overwrite:
             self._current = self.indexer._document(revid=revid)
-        return Revision(self, revid)
+        if return_rev:
+            return Revision(self, revid)
 
     def store_all_revisions(self, meta, data):
         """
--- a/MoinMoin/storage/middleware/protecting.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/middleware/protecting.py	Sun Feb 03 03:56:51 2013 +0100
@@ -256,15 +256,16 @@
     def get_revision(self, revid):
         return self[revid]
 
-    def store_revision(self, meta, data, overwrite=False, **kw):
+    def store_revision(self, meta, data, overwrite=False, return_rev=False, **kw):
         self.require(WRITE)
         if not self:
             self.require(CREATE)
         if overwrite:
             self.require(DESTROY)
-        rev = self.item.store_revision(meta, data, overwrite=overwrite, **kw)
+        rev = self.item.store_revision(meta, data, overwrite=overwrite, return_rev=return_rev, **kw)
         self.protector._clear_acl_cache()
-        return ProtectedRevision(self.protector, rev, p_item=self)
+        if return_rev:
+            return ProtectedRevision(self.protector, rev, p_item=self)
 
     def store_all_revisions(self, meta, data):
         self.require(DESTROY)
--- a/MoinMoin/storage/stores/__init__.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/stores/__init__.py	Sun Feb 03 03:56:51 2013 +0100
@@ -16,6 +16,7 @@
 from abc import abstractmethod
 from collections import Mapping, MutableMapping
 
+from MoinMoin.util.StringIOClosing import StringIO
 
 class StoreBase(Mapping):
     """
@@ -114,6 +115,22 @@
         """
 
 
+class BytesMutableStoreMixin(object):
+    """
+    mix this into a FileMutableStore to get a BytesMutableStore, like shown here:
+
+    class BytesStore(BytesMutableStoreMixin, FileStore, BytesMutableStoreBase):
+        # that's all, nothing more needed
+    """
+    def __getitem__(self, key):
+        with super(BytesMutableStoreMixin, self).__getitem__(key) as stream:
+            return stream.read()
+
+    def __setitem__(self, key, value):
+        with StringIO(value) as stream:
+            super(BytesMutableStoreMixin, self).__setitem__(key, stream)
+
+
 class FileMutableStoreBase(MutableStoreBase):
     @abstractmethod
     def __setitem__(self, key, stream):
@@ -124,3 +141,18 @@
               closing that file later. caller must not rely on some specific
               file pointer position after we return.
         """
+
+class FileMutableStoreMixin(object):
+    """
+    mix this into a BytesMutableStore to get a FileMutableStore, like shown here:
+
+    class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase)
+        # that's all, nothing more needed
+    """
+    def __getitem__(self, key):
+        value = super(FileMutableStoreMixin, self).__getitem__(key)
+        return StringIO(value)
+
+    def __setitem__(self, key, stream):
+        value = stream.read()
+        super(FileMutableStoreMixin, self).__setitem__(key, value)
--- a/MoinMoin/storage/stores/fs.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/stores/fs.py	Sun Feb 03 03:56:51 2013 +0100
@@ -14,10 +14,11 @@
 import errno
 import shutil
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 
-class _Store(MutableStoreBase):
+class FileStore(FileMutableStoreBase):
     """
     A simple filesystem-based store.
 
@@ -54,23 +55,6 @@
     def __delitem__(self, key):
         os.remove(self._mkpath(key))
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
-    def __getitem__(self, key):
-        try:
-            with open(self._mkpath(key), 'rb') as f:
-                return f.read() # better use get_file() and read smaller blocks for big files
-        except IOError as e:
-            if e.errno == errno.ENOENT:
-                raise KeyError(key)
-            raise
-
-    def __setitem__(self, key, value):
-        with open(self._mkpath(key), "wb") as f:
-            f.write(value)
-
-
-class FileStore(_Store, FileMutableStoreBase):
     def __getitem__(self, key):
         try:
             return open(self._mkpath(key), 'rb')
@@ -83,3 +67,7 @@
         with open(self._mkpath(key), "wb") as f:
             blocksize = 64 * 1024
             shutil.copyfileobj(stream, f, blocksize)
+
+
+class BytesStore(BytesMutableStoreMixin, FileStore, BytesMutableStoreBase):
+    """filesystem BytesStore"""
--- a/MoinMoin/storage/stores/kc.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/stores/kc.py	Sun Feb 03 03:56:51 2013 +0100
@@ -18,14 +18,14 @@
 from __future__ import absolute_import, division
 
 import os, errno
-from StringIO import StringIO
 
 from kyotocabinet import *
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     Kyoto cabinet based store.
     """
@@ -82,8 +82,6 @@
     def __delitem__(self, key):
         self._db.remove(key)
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         value = self._db.get(key)
         if value is None:
@@ -95,13 +93,5 @@
             raise KeyError("set error: " + str(self._db.error()))
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        value = self._db.get(key)
-        if value is None:
-            raise KeyError("get error: " + str(self._db.error()))
-        return StringIO(value)
-
-    def __setitem__(self, key, stream):
-        if not self._db.set(key, stream.read()):
-            raise KeyError("set error: " + str(self._db.error()))
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """Kyoto Cabinet FileStore"""
--- a/MoinMoin/storage/stores/kt.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/stores/kt.py	Sun Feb 03 03:56:51 2013 +0100
@@ -15,8 +15,6 @@
 import urllib
 from httplib import HTTPConnection
 
-from StringIO import StringIO
-
 from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
 
 
--- a/MoinMoin/storage/stores/memory.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/stores/memory.py	Sun Feb 03 03:56:51 2013 +0100
@@ -12,12 +12,11 @@
 
 from __future__ import absolute_import, division
 
-from StringIO import StringIO
-
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     A simple dict-based in-memory store. No persistence!
     """
@@ -48,8 +47,6 @@
     def __delitem__(self, key):
         del self._st[key]
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         return self._st[key]
 
@@ -57,9 +54,5 @@
         self._st[key] = value
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        return StringIO(self._st[key])
-
-    def __setitem__(self, key, stream):
-        self._st[key] = stream.read()
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """memory FileStore"""
--- a/MoinMoin/storage/stores/sqla.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/stores/sqla.py	Sun Feb 03 03:56:51 2013 +0100
@@ -10,18 +10,17 @@
 
 from __future__ import absolute_import, division
 
-from StringIO import StringIO
-
 from sqlalchemy import create_engine, select, MetaData, Table, Column, String, Binary
 from sqlalchemy.pool import StaticPool
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 KEY_LEN = 128
 VALUE_LEN = 1024 * 1024 # 1MB binary data
 
 
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     A simple dict-based in-memory store. No persistence!
     """
@@ -89,8 +88,6 @@
     def __delitem__(self, key):
         self.table.delete().where(self.table.c.key == key).execute()
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         value = select([self.table.c.value], self.table.c.key == key).execute().fetchone()
         if value is not None:
@@ -102,13 +99,5 @@
         self.table.insert().execute(key=key, value=value)
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        value = select([self.table.c.value], self.table.c.key == key).execute().fetchone()
-        if value is not None:
-            return StringIO(value[0])
-        else:
-            raise KeyError(key)
-
-    def __setitem__(self, key, stream):
-        self.table.insert().execute(key=key, value=stream.read())
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """sqlalchemy FileStore"""
--- a/MoinMoin/storage/stores/sqlite.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/storage/stores/sqlite.py	Sun Feb 03 03:56:51 2013 +0100
@@ -14,14 +14,14 @@
 
 from __future__ import absolute_import, division
 
-from StringIO import StringIO
 import zlib
 from sqlite3 import *
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     A simple sqlite3 based store.
     """
@@ -98,8 +98,6 @@
             value = zlib.decompress(value)
         return value
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         rows = list(self.conn.execute("select value from {0} where key=?".format(self.table_name), (key, )))
         if not rows:
@@ -113,16 +111,5 @@
             self.conn.execute('insert into {0} values (?, ?)'.format(self.table_name), (key, buffer(value)))
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        rows = list(self.conn.execute("select value from {0} where key=?".format(self.table_name), (key, )))
-        if not rows:
-            raise KeyError(key)
-        value = str(rows[0]['value'])
-        return StringIO(self._decompress(value))
-
-    def __setitem__(self, key, stream):
-        value = stream.read()
-        value = self._compress(value)
-        with self.conn:
-            self.conn.execute('insert into {0} values (?, ?)'.format(self.table_name), (key, buffer(value)))
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """sqlite FileStore"""
--- a/MoinMoin/templates/common.js	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/common.js	Sun Feb 03 03:56:51 2013 +0100
@@ -45,41 +45,6 @@
 $(document).ready(selected_link);
 
 
-// Insert Zero-Width-Space characters into long text strings of textNode elements.  Executed on page load.
-// Firefox does not support CSS with {word-wrap: break-word;} within tables.
-// As a result, Firefox may display tables with long urls or page names as very wide tables.
-// This function alters table cells by inserting a zero-width-space into long text strings after every 5 characters.
-// The moin-wordbreak class is intended for use on TD elements, but may be used on TABLE, TR, THEAD, TBODY, or TFOOT.
-function moinFirefoxWordBreak() {
-    "use strict";
-    // TODO:  Test for browser version when/if a future Firefox supports break-word within tables.
-    if (!$.browser.mozilla) {
-        return;
-    }
-    var child, words, parents, i, j;
-    // Only textNodes are of interest, but there is no way to select them directly.
-    // Select all elements with the moin-wordbreak class and add all selectable descendants of those elements.
-    // Then search for children that are textNodes; TDs or THs and elements descended from them are likely parents of textNodes.
-    parents = $(".moin-wordbreak").add(".moin-wordbreak *");
-    for (i = 0; i < parents.length; i += 1) {
-        child = parents[i].firstChild;
-        while (child) {
-            if (child.nodeType === 3) {
-                words = child.textContent.split(" ");
-                for (j = 0; j < words.length; j += 1) {
-                    // \u200B denotes a zero-width-space character (for easy testing, replace with a visible character like Q)
-                    words[j] = words[j].replace(/(.{5})/g, "$1\u200B");
-                }
-                child.textContent = words.join(" ");
-            }
-            child = child.nextSibling;
-        }
-    }
-}
-$(moinFirefoxWordBreak);
-
-
-
 // toggleComments is executed when user clicks a Comments button and conditionally on dom ready.
 var pageComments = null; // will hold list of elements with class "comment"
 function toggleComments() {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/crash.html	Sun Feb 03 03:56:51 2013 +0100
@@ -0,0 +1,15 @@
+<h1>{{ _("Oops!") }}</h1>
+<p>
+    {{ _("An error happened while processing your request.") }}
+</p>
+<p>
+    {{ _("More details have been logged. The following data is helpful for locating the corresponding log entries:") }}
+</p>
+<dl>
+    <dt>{{ _("Error ID") }}</dt>
+    <dd>{{ error_id }}</dd>
+    <dt>{{ _("Server time") }}</dt>
+    <dd>{{ server_time }}</dd>
+    <dt>{{ _("URL") }}</dt>
+    <dd>{{ url }} </dd>
+</dl>
--- a/MoinMoin/templates/global_history.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/global_history.html	Sun Feb 03 03:56:51 2013 +0100
@@ -44,10 +44,8 @@
         {% if bookmark_time %}
         <div class="moin-history-container">
             <div class="moin-history-container-header">
-                <span>
-                    <h2>{{ bookmark_time|datetimeformat }}</h2>
-                    <a href="{{ url_for('frontend.bookmark', time='del') }}">{{ _("Delete bookmark") }}</a>
-               </span>
+                <h2>{{ bookmark_time|datetimeformat }}</h2>
+                <a class="bookmark-link" href="{{ url_for('frontend.bookmark', time='del') }}">{{ _("Delete bookmark") }}</a>
             </div>
         </div>
         {% endif %}
--- a/MoinMoin/templates/history.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/history.html	Sun Feb 03 03:56:51 2013 +0100
@@ -18,59 +18,59 @@
     <div class="moin-clr"></div>
     <form action="{{ url_for('frontend.diff', item_name=item_name) }}" method="GET">
         <div id="moin-page-history">
-            <table class="zebra">
-                <thead>
-                    <tr>
-                        <th>{{ _("Name") }}</th>
-                        <th>{{ _("Rev.") }}</th>
-                        <th>{{ _("Timestamp") }}</th>
-                        <th>{{ _("Size") }}</th>
-                        <th><input type="submit" value="Diff" /></th>
-                        <th>{{ _("Editor") }}</th>
-                        <th>{{ _("Content-Type") }}</th>
-                        <th>{{ _("Comment") }}</th>
-                        <th colspan="6">{{ _("Actions") }}</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    {% for doc in history %}
-                    <tr>
-                        <td class="moin-wordbreak">{{ doc.name|join(' | ') }}</td>
-                        <td class="moin-integer monospaced">{{ doc.revid | shorten_id }}</td>
-                        <td>{{ doc.mtime|datetimeformat }}</td>
-                        <td class="moin-integer">{{ doc.size }}</td>
-                        <td>
-                            <div class="moin-hist-rev">
-                                <input type="radio" name="rev1" value="{{ doc.revid }}" />
-                                <input type="radio" name="rev2" value="{{ doc.revid }}" />
-                            </div>
-                        </td>
-                        <td class="moin-wordbreak">{{ utils.editor_info(doc) }}</td>
-                        <td class="moin-wordbreak">{{ doc.contenttype }}</td>
-                        <td class="moin-wordbreak">{{ doc.comment }}</td>
-                        <td><a href="{{ url_for('frontend.show_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('show') }}</a></td>
-                        <td><a href="{{ url_for('frontend.show_item_meta', item_name=doc.name[0], rev=doc.revid) }}">{{ _('meta') }}</a></td>
-                        <td><a href="{{ url_for('frontend.download_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('download') }}</a></td>
-                        <td><a href="{{ url_for('frontend.highlight_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('highlight') }}</a></td>
-                        {% if user.may.write(item_name) -%}
-                        <td><a href="{{ url_for('frontend.revert_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('revert') }}</a></td>
-                        {%- endif %}
-                        {% if user.may.destroy(item_name) -%}
-                        <td><a href="{{ url_for('frontend.destroy_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('destroy') }}</a></td>
-                        {%- endif %}
-                    </tr>
-                    {% endfor %}
-                </tbody>
-                {% if bookmark_time %}
-                <tfoot>
-                    <tr>
-                        <td colspan="2">Bookmark is set to</td>
-                        <td>{{ bookmark_time|datetimeformat }}</td>
-                        <td colspan="11"></td>
-                    </tr>
-                </tfoot>
-                {% endif %}
-            </table>
+        <table class="zebra">
+            <thead>
+                <tr>
+                    <th>{{ _("Name") }}</th>
+                    <th>{{ _("Rev.") }}</th>
+                    <th>{{ _("Timestamp") }}</th>
+                    <th class="moin-integer">{{ _("Size") }}</th>
+                    <th><input type="submit" value="Diff" /></th>
+                    <th>{{ _("Editor") }}</th>
+                    <th>{{ _("Content-Type") }}</th>
+                    <th>{{ _("Comment") }}</th>
+                    <th colspan="6">{{ _("Actions") }}</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for doc in history %}
+                <tr>
+                    <td class="moin-wordbreak">{{ doc.name|join(' | ') }}</td>
+                    <td>{{ doc.revid | shorten_id }}</td>
+                    <td>{{ doc.mtime|datetimeformat }}</td>
+                    <td class="moin-integer">{{ doc.size }}</td>
+                    <td>
+                        <div class="moin-hist-rev">
+                            <input type="radio" name="rev1" value="{{ doc.revid }}" />
+                            <input type="radio" name="rev2" value="{{ doc.revid }}" />
+                        </div>
+                    </td>
+                    <td class="moin-wordbreak">{{ utils.editor_info(doc) }}</td>
+                    <td class="moin-wordbreak">{{ doc.contenttype }}</td>
+                    <td class="moin-wordbreak">{{ doc.comment }}</td>
+                    <td><a href="{{ url_for('frontend.show_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('show') }}</a></td>
+                    <td><a href="{{ url_for('frontend.show_item_meta', item_name=doc.name[0], rev=doc.revid) }}">{{ _('meta') }}</a></td>
+                    <td><a href="{{ url_for('frontend.download_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('download') }}</a></td>
+                    <td><a href="{{ url_for('frontend.highlight_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('highlight') }}</a></td>
+                    {% if user.may.write(item_name) -%}
+                    <td><a href="{{ url_for('frontend.revert_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('revert') }}</a></td>
+                    {%- endif %}
+                    {% if user.may.destroy(item_name) -%}
+                    <td><a href="{{ url_for('frontend.destroy_item', item_name=doc.name[0], rev=doc.revid) }}">{{ _('destroy') }}</a></td>
+                    {%- endif %}
+                </tr>
+                {% endfor %}
+            </tbody>
+            {% if bookmark_time %}
+            <tfoot>
+                <tr>
+                    <td colspan="2">Bookmark is set to</td>
+                    <td>{{ bookmark_time|datetimeformat }}</td>
+                    <td colspan="11"></td>
+                </tr>
+            </tfoot>
+            {% endif %}
+        </table>
         </div>
         </form>
     {% endif %}
--- a/MoinMoin/templates/item_link_list.html	Sat Dec 01 15:20:52 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-{% extends theme("show.html") %}
-{% block content %}
-{% if headline %}
-<h1>{{ headline }}</h1>
-{% endif %}
-{% if item_names %}
-Total: {{ item_names|count }}
-<ul>
-    {% for item_name in item_names|sort %}
-    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
-    {% endfor %}
-</ul>
-{% endif %}
-{% endblock %}
--- a/MoinMoin/templates/layout.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/layout.html	Sun Feb 03 03:56:51 2013 +0100
@@ -39,7 +39,7 @@
         {% if user.valid -%}
             {% set avatar = user.avatar(20) %}
             {% if avatar %}
-                <img id="moin-avatar" rel="noreferrer" src="{{ avatar }}" />
+                <img id="moin-avatar" src="{{ avatar }}" />
             {%- endif %}
             {% if user.name -%}
                 {% set wiki_href, display_name, title, exists = theme_supp.userhome() %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/link_list_item_panel.html	Sun Feb 03 03:56:51 2013 +0100
@@ -0,0 +1,14 @@
+{% extends theme("show.html") %}
+{% block content %}
+{% if headline %}
+<h1>{{ headline }}</h1>
+{% endif %}
+{% if item_names %}
+Total: {{ item_names|count }}
+<ul>
+    {% for item_name in item_names|sort %}
+    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
+    {% endfor %}
+</ul>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/templates/link_list_no_item_panel.html	Sun Feb 03 03:56:51 2013 +0100
@@ -0,0 +1,14 @@
+{% extends theme("layout.html") %}
+{% block content %}
+{% if headline %}
+<h1>{{ headline }}</h1>
+{% endif %}
+{% if item_names %}
+Total: {{ item_names|count }}
+<ul>
+    {% for item_name in item_names|sort %}
+    <li><a href="{{ url_for('frontend.show_item', item_name=item_name) }}">{{ item_name }}</a></li>
+    {% endfor %}
+</ul>
+{% endif %}
+{% endblock %}
--- a/MoinMoin/templates/modify_text.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/modify_text.html	Sun Feb 03 03:56:51 2013 +0100
@@ -2,10 +2,6 @@
 
 {% macro data_editor(form, item_name) %}
 {{ base_editor(form) }}
-{% if direction %}
-    {{ gen.textarea(form['data_text'], lang=lang, dir=direction, rows=form.rows|string, cols=form.cols|string) }}
-{% else %}
-    {{ gen.textarea(form['data_text'], lang=lang, rows=form.rows|string, cols=form.cols|string) }}
-{% endif %}
+{{ gen.textarea(form['data_text'], rows=form.rows|string, cols=form.cols|string) }}
 <br />
 {% endmacro %}
--- a/MoinMoin/templates/modify_text_html.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/modify_text_html.html	Sun Feb 03 03:56:51 2013 +0100
@@ -6,5 +6,5 @@
 {% endmacro %}
 
 {% macro data_editor(form, item_name) %}
-{{ gen.textarea(form['data_text'], class='ckeditor', lang=lang, dir=direction, rows=form.rows|string, cols=form.cols|string) }}
+{{ gen.textarea(form['data_text'], class='ckeditor', rows=form.rows|string, cols=form.cols|string) }}
 {% endmacro %}
--- a/MoinMoin/templates/search.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/search.html	Sun Feb 03 03:56:51 2013 +0100
@@ -23,18 +23,20 @@
     </p>
     {% endif %}
     {{ gen.form.open(medium_search_form, id='moin-long-searchform', method='get', action=url_for('frontend.search', item_name=item_name)) }}
-        <div>
-            {{ forms.render(medium_search_form['q']) }}
-            {{ forms.render(medium_search_form['submit']) }}
-            {{ forms.render(medium_search_form['history']) }}
-            {{ forms.render_errors(medium_search_form) }}
-        </div>
+        <p>
+        {{ forms.render(medium_search_form['q']) }}
+        {{ forms.render(medium_search_form['submit']) }}
+        </p>
+        <p>
+        {{ forms.render(medium_search_form['history']) }}
+        {{ forms.render_errors(medium_search_form) }}
+        </p>
     {{ gen.form.close() }}
     {% if results is defined %}
-        <div>
+        {% if name_suggestions or content_suggestions %}
             <p>{{ _("name term suggestions: %(termlist)s", termlist=name_suggestions) }}</p>
             <p>{{ _("content term suggestions: %(termlist)s", termlist=content_suggestions) }}</p>
-        </div>
+        {% endif %}
         {% if results %}
             <div class="searchresults">
                 <table>
--- a/MoinMoin/templates/utils.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/templates/utils.html	Sun Feb 03 03:56:51 2013 +0100
@@ -23,12 +23,16 @@
 {% endmacro %}
 
 
-{% macro table(headings, rows) %}
+{% macro table(headings, rows, itemname_cols=[], url_cols=[]) %}
 <table class="zebra">
 <thead>
     <tr>
         {% for heading in headings %}
-        <th>{{ heading }}</th>
+            {% if rows and rows[0][loop.index0] is number %}
+                <th class="moin-integer">{{ heading }}</th>
+            {% else %}
+                <th>{{ heading }}</th>
+            {% endif %}
         {% endfor %}
     </tr>
 </thead>
@@ -38,6 +42,10 @@
         {% for col in row %}
             {% if col is number %}
                 <td class="moin-integer">{{ col }}</td>
+            {% elif loop.index in itemname_cols %}
+                <td><a href="{{ url_for('frontend.show_item', item_name=col) }}">{{ col }}</a></td>
+            {% elif loop.index in url_cols %}
+                <td><a href="{{ col }}">{{ col }}</a></td>
             {%- else -%}
                 <td>{{ col }}</td>
             {% endif %}
@@ -70,8 +78,10 @@
     </ul>
 {% endmacro %}
 
-{% macro render_subitem_navigation(itemname, newtab) %}
-    {% set subitems = theme_supp.subitem_index(itemname) %}
+{% macro render_subitem_navigation(itemname, newtab, subitems=None) %}
+    {% if not subitems %}
+        {% set subitems = theme_supp.subitem_index(itemname) %}
+    {% endif %}
     {% if caller %}
         {% set mycaller = caller %}
     {% endif %}
--- a/MoinMoin/themes/__init__.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/__init__.py	Sun Feb 03 03:56:51 2013 +0100
@@ -54,7 +54,7 @@
 def themed_error(e):
     item_name = request.view_args.get('item_name', u'')
     if e.code == 403:
-        title = L_('Access denied')
+        title = L_('Access Denied')
         description = L_('You are not allowed to access this resource.')
     else:
         # if we have no special code, we just return the HTTPException instance
@@ -220,7 +220,7 @@
         # Add sister pages (see http://usemod.com/cgi-bin/mb.pl?SisterSitesImplementationGuide )
         for sistername, sisterurl in self.cfg.sistersites:
             if is_local_wiki(sistername):
-                items.append(('sisterwiki current', sisterurl, sistername))
+                items.append(('sisterwiki current', sisterurl, sistername, ''))
             else:
                 cid = cache_key(usage="SisterSites", sistername=sistername)
                 sisteritems = app.cache.get(cid)
--- a/MoinMoin/themes/foobar/static/css/common.css	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/foobar/static/css/common.css	Sun Feb 03 03:56:51 2013 +0100
@@ -147,6 +147,7 @@
 .center { text-align: center; }
 .right { text-align: right; }
 .justify { text-align: justify; }
+.monospaced { font-family: monospace; }
 a.moin-www:before,a.moin-http:before,a.moin-https:before{content:url("../img/moin-www.png");margin:0 .2em;vertical-align:middle}
 a.moin-file:before,a.moin-ftp:before{content:url("../img/moin-ftp.png");margin:0 .2em;vertical-align:middle}
 a.moin-nntp:before,a.moin-news:before{content:url("../img/moin-news.png");margin:0 .2em;vertical-align:middle}
@@ -174,7 +175,7 @@
 .moin-diff-removed{background-color:#fff5ee;vertical-align:top;width:50%;white-space:pre-wrap;word-wrap:break-word;font-family:monospace;padding:.5em;}
 .moin-diff-removed span{background-color:#f1eeb9}
 body{color:#000;background-color:#d6d5d0;font-family:Helvetica,Arial,sans-serif;background-image:url("../img/base.png")}
-div p{margin:.2em .4em;font-size:1em;line-height:1.3em}
+p{margin:.2em .4em;font-size:1em;line-height:1.3em}
 #moin-global-tray{float:left;width:16.666666666666668%;padding:.5%;overflow:hidden}
 #moin-main-container{float:left;width:81.13333333333333%;padding:.5%}
 #moin-main-box{background-color:#f4f4f4;border:1px solid #ccc;border-radius:6px;box-shadow:2px 2px 4px #9d9d9b;overflow:hidden}
@@ -312,7 +313,6 @@
 .moin-history-trash:before{content:url("../img/moin-deleted.png")}
 .moin-history-rename:before{content:url("../img/moin-renamed.png")}
 .moin-history-copy:before{content:url("../img/moin-new.png")}
-.moin-integer{width:2%}
 .moin-inline-label{display:inline}
 .moin-history-links{width:5%;}
 .moin-history-links a{display:block}
@@ -350,7 +350,7 @@
 .searchresults dd,.searchresults p{font-size:.85em}
 .searchresults td{border-width:0}
 .searchresults p.info{margin-left:2%}
-.searchresults .searchhitinfobar{color:#51d443;margin-left:15px;margin-top:0}
+.searchresults .searchhitinfobar{margin-left:15px;margin-top:0}
 .searchresults .foundtext{margin-left:15px;margin-top:0}
 p.searchstats{font-size:.8em;text-align:right;width:100%;padding:2px}
 p.searchhint{background-color:#f4f4f4;border:1px solid #9d9d9b;padding:2px}
@@ -406,6 +406,7 @@
 table.zebra tbody tr,table.zebra > tr{background-color:#d9d9d9}
 table.zebra th,table.zebra td{border:none}
 table.zebra td form input{width:100%}
+table.zebra tbody tr:nth-child(odd),table.zebra > tr:nth-child(odd){background-color:#e8e8e8}
 pre{font-family:monospace;white-space:pre-wrap;word-wrap:break-word;border:1px solid #ccc;background-color:#d6d5d0;margin:.5em;padding:5px;clear:both;}
 pre.comment{background-color:#2d2d2d;color:#fff;margin:0;padding:0;border:0;}
 pre.comment:before{content:url("../img/attention.png")}
@@ -415,7 +416,7 @@
 .moin-subitem-navigation{overflow:hidden;word-wrap:break-word;}
 .moin-subitem-navigation ul{display:block;}
 .moin-subitem-navigation ul li{padding:.4em !important}
-.moin-subitem-navigation ul a{display:initial !important;color:#1e90ff;}
+.moin-subitem-navigation ul a{display:inline !important;color:#1e90ff;}
 .moin-subitem-navigation ul a:hover{text-decoration:underline}
 .moin-subitem-navigation .sep{font-size:1.2em;margin-left:0}
 .moin-subitem-navigation li ul{display:none;border-left:1px dotted #ccc;padding-left:5% !important}
@@ -440,7 +441,9 @@
 .moin-permalink{display:none}
 sub{vertical-align:sub}
 sup{vertical-align:super}
+ins{background-color:inherit;text-decoration:underline}
 .moin-big{font-size:1.17em}
 .moin-small{font-size:.83em}
+.moin-integer{text-align:right;padding-right:1em;width:2%}
 @media print{#moin-global-tray,#moin-local-panel,#moin-footer,#moin-search{display:none}
 }
--- a/MoinMoin/themes/foobar/static/css/stylus/color_palette.styl	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/foobar/static/css/stylus/color_palette.styl	Sun Feb 03 03:56:51 2013 +0100
@@ -13,6 +13,7 @@
 shadow_color = #9d9d9b
 table_bg_color = #d9d9d9
 table_header_color = #808080
+table_row_color = #e8e8e8
 link_color = #1E90FF
 hover_color = #007cef
 active_color = #51d443
--- a/MoinMoin/themes/foobar/static/css/stylus/main.styl	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/foobar/static/css/stylus/main.styl	Sun Feb 03 03:56:51 2013 +0100
@@ -53,7 +53,7 @@
     font-family font_family
     background-image url(../img/base.png)
 
-div p
+p
     margin .2em .4em
     font-size 1em
     line-height 1.3em
@@ -666,9 +666,6 @@
 .moin-history-copy:before
     content url('../img/moin-new.png')
 
-.moin-integer
-    width 2%
-
 .moin-inline-label
     display inline
 
@@ -814,7 +811,6 @@
         margin-left 2%
 
 .searchresults .searchhitinfobar
-    color active_color
     margin-left 15px
     margin-top 0
 
@@ -979,6 +975,9 @@
         form
             input
                 width 100%
+    tbody tr:nth-child(odd),
+    > tr:nth-child(odd)
+        background-color table_row_color
 
 pre
     font-family monospace
@@ -1021,7 +1020,7 @@
         li
             padding .4em !important
         a
-            display initial !important
+            display inline !important
             color link_color
             &:hover
                 text-decoration underline
@@ -1155,10 +1154,17 @@
     vertical-align sub
 sup
     vertical-align super
+ins
+    background-color inherit // needed for "/* __My Comment__ */"
+    text-decoration underline
 .moin-big
     font-size 1.17em
 .moin-small
     font-size .83em
+.moin-integer
+    text-align right
+    padding-right 1em
+    width 2%
 
 /* fix tests -- currently checking <big> and <small> */
 @media print
--- a/MoinMoin/themes/foobar/static/css/stylus/userstyles.css	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/foobar/static/css/stylus/userstyles.css	Sun Feb 03 03:56:51 2013 +0100
@@ -12,3 +12,4 @@
 .center { text-align: center; }
 .right { text-align: right; }
 .justify { text-align: justify; }
+.monospaced { font-family: monospace; }
--- a/MoinMoin/themes/foobar/templates/layout.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/foobar/templates/layout.html	Sun Feb 03 03:56:51 2013 +0100
@@ -65,7 +65,7 @@
         {% if user.valid -%}
 	    {% set avatar = user.avatar(20) %}
             {% if avatar %}
-                <li><img id="moin-avatar" rel="noreferrer" src="{{ avatar }}" /></li>
+                <li><img id="moin-avatar" src="{{ avatar }}" /></li>
             {%- endif %}
             {% if user.name -%}
                 {% set wiki_href, aliasname, title, exists = theme_supp.userhome() %}
--- a/MoinMoin/themes/foobar/templates/show.html	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/foobar/templates/show.html	Sun Feb 03 03:56:51 2013 +0100
@@ -43,11 +43,14 @@
             </ul>
         </li>
         {% endif %}
+        {% set subitems = theme_supp.subitem_index(item_name) %}
+        {% if subitems %}
         <li>
             {% block subitem_navigation %}
-            {{ utils.render_subitem_navigation(item_name, False) }}
+            {{ utils.render_subitem_navigation(item_name, False, subitems=subitems) }}
             {% endblock %}
         </li>
+        {% endif %}
     </ul>
 {% endblock %}
 
--- a/MoinMoin/themes/modernized/static/css/common.css	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/modernized/static/css/common.css	Sun Feb 03 03:56:51 2013 +0100
@@ -94,6 +94,7 @@
 .center { text-align: center; }
 .right { text-align: right; }
 .justify { text-align: justify; }
+.monospaced { font-family: monospace; }
 a.moin-www:before,a.moin-http:before,a.moin-https:before{content:url("../img/moin-www.png");margin:0 .2em;vertical-align:middle}
 a.moin-file:before,a.moin-ftp:before{content:url("../img/moin-ftp.png");margin:0 .2em;vertical-align:middle}
 a.moin-nntp:before,a.moin-news:before{content:url("../img/moin-news.png");margin:0 .2em;vertical-align:middle}
@@ -182,13 +183,13 @@
 .vi { color: #19177C } /* Name.Variable.Instance */
 .il { color: #666666 } /* Literal.Number.Integer.Long */
 html{color:#000;font-family:Helvetica,Arial,sans-serif}
-div,p{margin:1em 0;font-size:1em;line-height:1.3em}
+p{margin:1em 0;font-size:1em;line-height:1.3em}
 sub{vertical-align:sub}
 sup{vertical-align:super}
-ins{background-color:#fff;text-decoration:underline}
+ins{background-color:inherit;text-decoration:underline}
 .moin-big{font-size:1.17em}
 .moin-small{font-size:.83em}
-.moin-integer{text-align:right;padding-right:1em}
+.moin-integer{text-align:right;padding-right:1em;width:2%}
 .moin-inline-label{display:inline}
 h1{font-size:2em;margin:.67em 0;border-bottom:5px solid #4e7da9;padding-bottom:5px}
 h2{font-size:1.5em;margin:.75em 0;padding-bottom:4px}
@@ -229,7 +230,6 @@
 pre{border:1px solid #4e7da9;background-color:#dfdfdf;padding:5px;clear:both;font-family:monospace;margin:.33em 0;white-space:pre-wrap}
 pre.comment{background-color:#708090;color:#fff;padding:0;margin:0;border:0}
 pre.comment:before{content:url("../img/attention.png")}
-.monospaced{font-family:monospace}
 table{margin:1%;border-collapse:collapse}
 th,td{padding:.3em .4em;vertical-align:middle;text-align:left}
 th{border:1px solid #4e7da9;background-color:#708090;text-align:center;color:#d8dfe9}
@@ -274,7 +274,7 @@
 .moin-form button,.moin-form input[type="submit"]{clear:both;display:table;margin:auto}
 form[name="delete_item"] dd input,form[name="rename_item"] dd input,form[name="destroy_item"] dd input{width:100%}
 form[name="delete_item"] dt,form[name="rename_item"] dt,form[name="destroy_item"] dt{width:20%}
-#moin-modify dd{width:100%;}
+#moin-modify dd{width:100%;float:none;}
 #moin-modify dd input{width:99%}
 #moin-modify dt{width:auto;text-align:left}
 #moin-modify textarea{width:99.5%}
@@ -295,7 +295,7 @@
 .searchresults dd,.searchresults p{font-size:.85em}
 .searchresults td{border-width:0}
 .searchresults p.info{margin-left:2%}
-.searchresults .searchhitinfobar{color:#939393;margin-left:15px;margin-top:0}
+.searchresults .searchhitinfobar{margin-left:15px;margin-top:0}
 .searchresults .foundtext{margin-left:15px;margin-top:0}
 p.searchstats{font-size:.8em;text-align:right;width:100%;padding:2px}
 p.searchhint{background-color:#eef1f6;border:1px solid #4e7da9;padding:2px}
@@ -331,7 +331,6 @@
 .moin-history-trash:before{content:url("../img/moin-deleted.png")}
 .moin-history-rename:before{content:url("../img/moin-renamed.png")}
 .moin-history-copy:before{content:url("../img/moin-new.png")}
-.moin-integer{width:2%}
 .moin-history-links{width:5%}
 .moin-history-links a{display:block;margin-bottom:4px}
 .moin-history-editortext,.moin-history-links span,.moin-history-comment span{color:#000;display:block;margin-bottom:5px}
@@ -450,7 +449,7 @@
 #moin-searchform div{margin:-6px 5px 0 0}
 #moin-long-searchform{padding:0;font-size:.82em;text-align:left}
 #moin-long-searchform div{margin:0}
-#moin-search-query{width:80%}
+#moin-search-query{width:50%}
 #moin-logo{float:left;margin:2px 10px;padding:0;font-size:1.4em;line-height:1em;font-weight:bold}
 #moin-logo img{vertical-align:middle}
 #moin-logo a{color:#fff;text-decoration:none}
--- a/MoinMoin/themes/modernized/static/css/stylus/main.styl	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/modernized/static/css/stylus/main.styl	Sun Feb 03 03:56:51 2013 +0100
@@ -33,7 +33,7 @@
     color font_color
     font-family Helvetica, Arial, sans-serif
 
-div, p
+p
     margin 1em 0
     font-size 1em
     line-height 1.3em
@@ -45,7 +45,7 @@
     vertical-align super
 
 ins
-    background-color page_color
+    background-color inherit // needed for "/* __My Comment__ */"
     text-decoration underline
 
 .moin-big
@@ -57,6 +57,7 @@
 .moin-integer
     text-align right
     padding-right 1em
+    width 2%
 
 .moin-inline-label
     display inline
@@ -212,9 +213,6 @@
 pre.comment:before
     content url(../img/attention.png)
 
-.monospaced
-    font-family monospace
-
 table
     margin 1%
     border-collapse collapse
@@ -417,6 +415,7 @@
 #moin-modify
     dd
         width 100%
+        float none
         input
             width 99%
     dt
@@ -506,7 +505,6 @@
     margin-left 2%
 
 .searchresults .searchhitinfobar
-    color nonexistent_link_color
     margin-left 15px
     margin-top 0
 
@@ -662,9 +660,6 @@
 .moin-history-copy:before
     content url('../img/moin-new.png')
 
-.moin-integer
-    width 2%
-
 .moin-history-links
     width 5%
 
@@ -1235,7 +1230,7 @@
     margin 0
 
 #moin-search-query
-    width 80%
+    width 50%
 
 #moin-logo
     float left
--- a/MoinMoin/themes/modernized/static/css/stylus/userstyles.css	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/themes/modernized/static/css/stylus/userstyles.css	Sun Feb 03 03:56:51 2013 +0100
@@ -12,3 +12,4 @@
 .center { text-align: center; }
 .right { text-align: right; }
 .justify { text-align: justify; }
+.monospaced { font-family: monospace; }
--- a/MoinMoin/user.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/user.py	Sun Feb 03 03:56:51 2013 +0100
@@ -1,5 +1,5 @@
 # Copyright: 2000-2004 Juergen Hermann <jh@web.de>
-# Copyright: 2003-2012 MoinMoin:ThomasWaldmann
+# Copyright: 2003-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2007 MoinMoin:JohannesBerg
 # Copyright: 2007 MoinMoin:HeinrichWendel
 # Copyright: 2008 MoinMoin:ChristopherDenter
@@ -33,14 +33,16 @@
 
 from whoosh.query import Term, And, Or
 
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
 from MoinMoin import wikiutil
 from MoinMoin.config import CONTENTTYPE_USER
 from MoinMoin.constants.keys import *
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.mail import sendmail
 from MoinMoin.util.interwiki import getInterwikiHome, getInterwikiName, is_local_wiki
-from MoinMoin.util.crypto import crypt_password, upgrade_password, valid_password, \
-                                 generate_token, valid_token, make_uuid
+from MoinMoin.util.crypto import generate_token, valid_token, make_uuid
 from MoinMoin.storage.error import NoSuchItemError, ItemAlreadyExistsError, NoSuchRevisionError
 
 
@@ -68,11 +70,7 @@
         if pw_error:
             return _("Password not acceptable: %(msg)s", msg=pw_error)
 
-    try:
-        theuser.set_password(password, is_encrypted)
-    except UnicodeError as err:
-        # Should never happen
-        return "Can't encode password: %(msg)s" % dict(msg=str(err))
+    theuser.set_password(password, is_encrypted)
 
     # try to get the email, for new users it is required
     if validate and not email:
@@ -438,20 +436,37 @@
         if not pw_hash or not password:
             return False, False
 
-        # check the password against the password hash
-        if not valid_password(password, pw_hash):
-            return False, False
+        pwd_context = self._cfg.cache.pwd_context
+        password_correct = False
+        recomputed_hash = None
+        try:
+            password_correct, recomputed_hash = pwd_context.verify_and_update(password, pw_hash)
+        except (ValueError, TypeError) as err:
+            logging.error('in user profile %r, verifying the passlib pw hash raised an Exception [%s]' % (self.name, str(err)))
+        else:
+            if recomputed_hash is not None:
+                data[ENC_PASSWORD] = recomputed_hash
+        return password_correct, bool(recomputed_hash)
 
-        new_pw_hash = upgrade_password(password, pw_hash)
-        if not new_pw_hash:
-            return True, False
+    def set_password(self, password, is_encrypted=False, salt=None):
+        """
+        Set or update the password (hash) stored for this user.
 
-        data[ENC_PASSWORD] = new_pw_hash
-        return True, True
-
-    def set_password(self, password, is_encrypted=False):
-        if not is_encrypted:
-            password = crypt_password(password)
+        :param password: the new password (or pw hash)
+                         giving an empty string or None as password will invalidate the stored
+                         password hash (meaning that it will not match against any given password)
+        :param is_encrypted: if False (default), the password is given as plaintext and will be
+                             "encrypted" (hashed) before getting stored.
+                             if True, the already "encrypted" password hash is given in param
+                             password and will be stored "as is" - this is mainly useful for tests.
+        :param salt: if None (default), passlib will generate and use a random salt.
+                     Otherwise, the given salt will be used - this is mainly useful for tests.
+        """
+        if not password:
+            # invalidate the pw hash
+            password = ''
+        elif not is_encrypted:
+            password = self._cfg.cache.pwd_context.encrypt(password, salt=salt)
         self.profile[ENC_PASSWORD] = password
         # Invalidate all other browser sessions except this one.
         session['user.session_token'] = self.generate_session_token(False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/StringIOClosing.py	Sun Feb 03 03:56:51 2013 +0100
@@ -0,0 +1,15 @@
+from StringIO import StringIO as StringIOBase
+
+
+class StringIO(StringIOBase):
+    """
+    same as StringIO from stdlib, but enhanced with a context manager, so it
+    can be used within a "with" statement and gets automatically closed when
+    the with-block is left. The standard "file" object behaves that way, so
+    a StringIO "file emulation" should behave the same.
+    """
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_tb):
+        self.close()
--- a/MoinMoin/util/_tests/test_crypto.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/util/_tests/test_crypto.py	Sun Feb 03 03:56:51 2013 +0100
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright: 2011 by MoinMoin:ThomasWaldmann
+# Copyright: 2011-2013 by MoinMoin:ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
@@ -29,49 +29,6 @@
         assert result == expected, ('Expected length "%(expected)s" but got "%(result)s"') % locals()
 
 
-class TestEncodePassword(object):
-    """crypto: encode passwords tests"""
-
-    def testAscii(self):
-        """user: encode ascii password"""
-        # u'MoinMoin' and 'MoinMoin' should be encoded to same result
-        expected = "{SSHA256}n0JB8FCTQCpQeg0bmdgvTGwPKvxm8fVNjSRD+JGNs50xMjM0NQ=="
-
-        result = crypto.crypt_password("MoinMoin", salt='12345')
-        assert result == expected
-        result = crypto.crypt_password(u"MoinMoin", salt='12345')
-        assert result == expected
-
-    def testUnicode(self):
-        """ user: encode unicode password """
-        result = crypto.crypt_password(u'סיסמה סודית בהחלט', salt='12345') # Hebrew
-        expected = "{SSHA256}pdYvYv+4Vph259sv/HAm7zpZTv4sBKX9ITOX/m00HMsxMjM0NQ=="
-        assert result == expected
-
-    def testupgradepassword(self):
-        """ return new password hash with better hash """
-        result = crypto.upgrade_password(u'MoinMoin', "junk_hash")
-        assert result.startswith('{SSHA256}')
-
-    def testvalidpassword(self):
-        """ validate user password """
-        hash_val = crypto.crypt_password(u"MoinMoin", salt='12345')
-        result = crypto.valid_password(u'MoinMoin', hash_val)
-        assert result
-        with pytest.raises(ValueError):
-            invalid_result = crypto.valid_password("MoinMoin", '{junk_value}')
-
-    def testvalidpassword2(self):
-        """ validate user password """
-        hash_val = crypto.crypt_password(u"MoinMoin")
-        result = crypto.valid_password('MoinMoin', hash_val)
-        assert result
-        result = crypto.valid_password('WrongPassword', hash_val)
-        assert not result
-        with pytest.raises(ValueError):
-            invalid_result = crypto.valid_password("MoinMoin", '{junk_value}')
-
-
 class TestToken(object):
     """ tests for the generated tokens """
 
@@ -96,6 +53,10 @@
         result = crypto.valid_token(test_key, test_token)
         assert not result
 
+
+class TestCacheKey(object):
+    """ tests for cache key generation """
+
     def test_cache_key(self):
         """ The key must be different for different <kw> """
         test_kw1 = {'MoinMoin': 'value1'}
@@ -104,4 +65,5 @@
         result2 = crypto.cache_key(**test_kw2)
         assert result1 != result2, ("Expected different keys for different <kw> but got the same")
 
+
 coverage_modules = ['MoinMoin.util.crypto']
--- a/MoinMoin/util/_tests/test_md5crypt.py	Sat Dec 01 15:20:52 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright: 2011 Prashant Kumar <contactprashantat AT gmail DOT com>
-# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
-
-"""
-MoinMoin - MoinMoin.util.md5crypt Tests
-"""
-
-
-import pytest
-from MoinMoin.util import md5crypt
-
-def test_unix_md5_crypt():
-    # when magic != None
-    result = md5crypt.unix_md5_crypt('test_pass', 'Moin_test', '$test_magic$')
-    expected = '$test_magic$Moin_tes$JRfmeHgnmCVhVYW.bTtiY1'
-    assert result == expected
-
-    # when magic == None
-    result = md5crypt.unix_md5_crypt('test_pass', 'Moin_test', None)
-    expected = '$1$Moin_tes$hArc67BzmDWtyWWKO5uxQ1'
-    assert result == expected
-
-def test_apache_md5_crypt():
-    # Here magic == '$apr1$'
-    result = md5crypt.apache_md5_crypt('test_pass', 'Moin_test')
-    expected = '$apr1$Moin_tes$4/5zV8nADrNv3BJcY1rZX1'
-    assert result == expected
--- a/MoinMoin/util/_tests/test_mimetype.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/util/_tests/test_mimetype.py	Sun Feb 03 03:56:51 2013 +0100
@@ -38,7 +38,6 @@
         for test_format, test_mimetype in test:
             result = MimeType_obj.parse_format(test_format)
             assert result == test_mimetype
-        result = MimeType_obj.parse_format(test_format)
 
     def test_mime_type(self):
         test = [
--- a/MoinMoin/util/crypto.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/util/crypto.py	Sun Feb 03 03:56:51 2013 +0100
@@ -1,9 +1,4 @@
-# Copyright: 2000-2004 Juergen Hermann <jh@web.de>
-# Copyright: 2003-2011 MoinMoin:ThomasWaldmann
-# Copyright: 2007 MoinMoin:JohannesBerg
-# Copyright: 2007 MoinMoin:HeinrichWendel
-# Copyright: 2008 MoinMoin:ChristopherDenter
-# Copyright: 2010 MoinMoin:DiogenesAugusto
+# Copyright: 2012-2013 MoinMoin:ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
@@ -11,12 +6,6 @@
 
 Features:
 
-- generate strong, salted cryptographic password hashes for safe pw storage
-- verify cleartext password against any supported crypto (see METHODS)
-- support old (weak) password crypto so one can import existing password
-  databases
-- supports password hash upgrades to stronger methods if the cleartext
-  password is available (usually at login time)
 - generate password recovery tokens
 - verify password recovery tokens
 - generate random strings of given length (for salting)
@@ -26,136 +15,36 @@
 
 from __future__ import absolute_import, division
 
-import base64
 import hashlib
 import hmac
-import random
 import time
 
-# Note: have the (strong) method that crypt_password() uses at index 0:
-METHODS = ['{SSHA256}', '{SSHA}', '{SHA}', ]
-
-try:
-    from . import md5crypt
-    METHODS.extend(['{APR1}', '{MD5}', ])
-except ImportError:
-    pass
-
-try:
-    import crypt
-    METHODS.extend(['{DES}', ])
-except ImportError:
-    pass
-
 from uuid import uuid4
 
 make_uuid = lambda: unicode(uuid4().hex)
 UUID_LEN = len(make_uuid())
 
-# random stuff
+from passlib.utils import rng, getrandstr, getrandbytes, consteq, generate_password
+
 
 def random_string(length, allowed_chars=None):
     """
     Generate a random string with given length consisting of the given characters.
 
+    Note: this is now just a little wrapper around passlib's randomness code.
+
     :param length: length of the string
     :param allowed_chars: string with allowed characters or None
                           to indicate all 256 byte values should be used
     :returns: random string
     """
     if allowed_chars is None:
-        s = ''.join([chr(random.randint(0, 255)) for dummy in xrange(length)])
+        s = getrandbytes(rng, length)
     else:
-        s = ''.join([random.choice(allowed_chars) for dummy in xrange(length)])
+        s = getrandstr(rng, allowed_chars, length)
     return s
 
 
-# password stuff
-
-def crypt_password(password, salt=None):
-    """
-    Crypt/Hash a cleartext password
-
-    :param password: cleartext password [unicode]
-    :param salt: salt for the password [str] or None to generate a random salt
-    :rtype: str
-    :returns: the SSHA256 password hash
-    """
-    password = password.encode('utf-8')
-    if salt is None:
-        salt = random_string(32)
-    assert isinstance(salt, str)
-    h = hashlib.new('sha256', password)
-    h.update(salt)
-    return '{SSHA256}' + base64.encodestring(h.digest() + salt).rstrip()
-
-
-def upgrade_password(password, pw_hash):
-    """
-    Upgrade a password to a better hash, if needed
-
-    :param password: cleartext password [unicode]
-    :param pw_hash: password hash (with hash type prefix)
-    :rtype: str
-    :returns: new password hash (or None, if unchanged)
-    """
-    if not pw_hash.startswith('{SSHA256}'):
-        # pw_hash using some old hash method, upgrade to better method
-        return crypt_password(password)
-
-
-def valid_password(password, pw_hash):
-    """
-    Validate a user password.
-
-    :param password: cleartext password to verify [unicode]
-    :param pw_hash: password hash (with hash type prefix)
-    :rtype: bool
-    :returns: password is valid
-    """
-    # encode password
-    pw_utf8 = password.encode('utf-8')
-
-    for method in METHODS:
-        if pw_hash.startswith(method):
-            d = pw_hash[len(method):]
-            if method == '{SSHA256}':
-                ph = base64.decodestring(d)
-                # ph is of the form "<hash><salt>"
-                salt = ph[32:]
-                h = hashlib.new('sha256', pw_utf8)
-                h.update(salt)
-                enc = base64.encodestring(h.digest() + salt).rstrip()
-            elif method == '{SSHA}':
-                ph = base64.decodestring(d)
-                # ph is of the form "<hash><salt>"
-                salt = ph[20:]
-                h = hashlib.new('sha1', pw_utf8)
-                h.update(salt)
-                enc = base64.encodestring(h.digest() + salt).rstrip()
-            elif method == '{SHA}':
-                h = hashlib.new('sha1', pw_utf8)
-                enc = base64.encodestring(h.digest()).rstrip()
-            elif method == '{APR1}':
-                # d is of the form "$apr1$<salt>$<hash>"
-                salt = d.split('$')[2]
-                enc = md5crypt.apache_md5_crypt(pw_utf8, salt.encode('ascii'))
-            elif method == '{MD5}':
-                # d is of the form "$1$<salt>$<hash>"
-                salt = d.split('$')[2]
-                enc = md5crypt.unix_md5_crypt(pw_utf8, salt.encode('ascii'))
-            elif method == '{DES}':
-                # d is 2 characters salt + 11 characters hash
-                salt = d[:2]
-                enc = crypt.crypt(pw_utf8, salt.encode('ascii'))
-            else:
-                raise ValueError("missing password hash method {0} handler".format(method))
-            return pw_hash == method + enc
-    else:
-        idx = pw_hash.index('}') + 1
-        raise ValueError("unsupported password hash method {0!r}".format(pw_hash[:idx]))
-
-
 # password recovery token
 
 def generate_token(key=None, stamp=None):
@@ -176,18 +65,18 @@
     :param key: give it to recompute some specific token for verification
     :param stamp: give it to recompute some specific token for verification
     :rtype: 2-tuple
-    :returns: key, token
+    :returns: key, token (both unicode)
     """
     if key is None:
-        key = random_string(64, "abcdefghijklmnopqrstuvwxyz0123456789")
+        key = generate_password(size=32)
     if stamp is None:
         stamp = int(time.time())
-    h = hmac.new(str(key), str(stamp), digestmod=hashlib.sha1).hexdigest()
-    token = str(stamp) + '-' + h
-    return key, token
+    h = hmac.new(str(key), str(stamp), digestmod=hashlib.sha256).hexdigest()
+    token = u"{0}-{1}".format(stamp, h)
+    return unicode(key), token
 
 
-def valid_token(key, token, timeout=12*60*60):
+def valid_token(key, token, timeout=2*60*60):
     """
     check if token is valid with respect to the secret key,
     the token must not be older than timeout seconds.
@@ -208,7 +97,7 @@
     if timeout and stamp + timeout < time.time():
         return False
     expected_token = generate_token(key, stamp)[1]
-    return token == expected_token
+    return consteq(token, expected_token)
 
 
 # miscellaneous
--- a/MoinMoin/util/md5crypt.py	Sat Dec 01 15:20:52 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,172 +0,0 @@
-#########################################################
-# md5crypt.py
-#
-# 0423.2000 by michal wallace http://www.sabren.com/
-# based on perl's Crypt::PasswdMD5 by Luis Munoz (lem@cantv.net)
-# based on /usr/src/libcrypt/crypt.c from FreeBSD 2.2.5-RELEASE
-#
-# MANY THANKS TO
-#
-#  Carey Evans - http://home.clear.net.nz/pages/c.evans/
-#  Dennis Marti - http://users.starpower.net/marti1/
-#
-#  For the patches that got this thing working!
-#
-#########################################################
-"""md5crypt.py - Provides interoperable MD5-based crypt() function
-
-SYNOPSIS
-
-        import md5crypt.py
-
-        cryptedpassword = md5crypt.md5crypt(password, salt);
-
-DESCRIPTION
-
-unix_md5_crypt() provides a crypt()-compatible interface to the
-rather new MD5-based crypt() function found in modern operating systems.
-It's based on the implementation found on FreeBSD 2.2.[56]-RELEASE and
-contains the following license in it:
-
- "THE BEER-WARE LICENSE" (Revision 42):
- <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
- can do whatever you want with this stuff. If we meet some day, and you think
- this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
-
-apache_md5_crypt() provides a function compatible with Apache's
-.htpasswd files. This was contributed by Bryan Hart <bryan@eai.com>.
-
-"""
-
-MAGIC = '$1$'                   # Magic string
-ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-
-try:
-    import hashlib
-    hash_md5 = hashlib.md5
-except ImportError:
-    # maybe we have python < 2.5 (no hashlib)
-    import md5
-    hash_md5 = md5.new
-
-
-def to64(v, n):
-    ret = ''
-    while (n - 1 >= 0):
-        n = n - 1
-        ret = ret + ITOA64[v & 0x3f]
-        v = v >> 6
-    return ret
-
-
-def apache_md5_crypt(pw, salt):
-    # change the Magic string to match the one used by Apache
-    return unix_md5_crypt(pw, salt, '$apr1$')
-
-
-def unix_md5_crypt(pw, salt, magic=None):
-
-    if magic is None:
-        magic = MAGIC
-
-    # Take care of the magic string if present
-    if salt[:len(magic)] == magic:
-        salt = salt[len(magic):]
-
-
-    # salt can have up to 8 characters:
-    import string
-    salt = string.split(salt, '$', 1)[0]
-    salt = salt[:8]
-
-    ctx = pw + magic + salt
-
-    md5 = hash_md5()
-    md5.update(pw + salt + pw)
-    final = md5.digest()
-
-    for pl in range(len(pw), 0, -16):
-        if pl > 16:
-            ctx = ctx + final[:16]
-        else:
-            ctx = ctx + final[:pl]
-
-
-    # Now the 'weird' xform (??)
-
-    i = len(pw)
-    while i:
-        if i & 1:
-            ctx = ctx + chr(0)  #if ($i & 1) { $ctx->add(pack("C", 0)); }
-        else:
-            ctx = ctx + pw[0]
-        i = i >> 1
-
-    md5 = hash_md5()
-    md5.update(ctx)
-    final = md5.digest()
-
-    # The following is supposed to make
-    # things run slower.
-
-    # my question: WTF???
-
-    for i in range(1000):
-        ctx1 = ''
-        if i & 1:
-            ctx1 = ctx1 + pw
-        else:
-            ctx1 = ctx1 + final[:16]
-
-        if i % 3:
-            ctx1 = ctx1 + salt
-
-        if i % 7:
-            ctx1 = ctx1 + pw
-
-        if i & 1:
-            ctx1 = ctx1 + final[:16]
-        else:
-            ctx1 = ctx1 + pw
-
-
-        md5 = hash_md5()
-        md5.update(ctx1)
-        final = md5.digest()
-
-
-    # Final xform
-
-    passwd = ''
-
-    passwd = passwd + to64((int(ord(final[0])) << 16)
-                           |(int(ord(final[6])) << 8)
-                           |(int(ord(final[12]))), 4)
-
-    passwd = passwd + to64((int(ord(final[1])) << 16)
-                           |(int(ord(final[7])) << 8)
-                           |(int(ord(final[13]))), 4)
-
-    passwd = passwd + to64((int(ord(final[2])) << 16)
-                           |(int(ord(final[8])) << 8)
-                           |(int(ord(final[14]))), 4)
-
-    passwd = passwd + to64((int(ord(final[3])) << 16)
-                           |(int(ord(final[9])) << 8)
-                           |(int(ord(final[15]))), 4)
-
-    passwd = passwd + to64((int(ord(final[4])) << 16)
-                           |(int(ord(final[10])) << 8)
-                           |(int(ord(final[5]))), 4)
-
-    passwd = passwd + to64((int(ord(final[11]))), 2)
-
-
-    return magic + salt + '$' + passwd
-
-
-## assign a wrapper function:
-md5crypt = unix_md5_crypt
-
-if __name__ == "__main__":
-    print unix_md5_crypt("cat", "hat")
--- a/MoinMoin/util/mime.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/MoinMoin/util/mime.py	Sun Feb 03 03:56:51 2013 +0100
@@ -6,7 +6,9 @@
 """
 
 
-class Type(object):
+from collections import namedtuple
+
+class Type(namedtuple('Type', 'type subtype parameters')):
     """
     :ivar type: Type part
     :type type: unicode
@@ -18,7 +20,7 @@
 
     __token_allowed = s = frozenset(r"""!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~""")
 
-    def __init__(self, _type=None, type=None, subtype=None, parameters=None):
+    def __new__(cls, _type=None, type=None, subtype=None, parameters=None):
         """
         :param _type: Type object or string representation
         :keyword type: Type part
@@ -26,36 +28,28 @@
         :keyword parameters: Parameters part
         """
         if isinstance(_type, Type):
-            self.type = _type.type
-            self.subtype = _type.subtype
-            self.parameters = _type.parameters.copy()
-
+            new_type, new_subtype, new_parameters = _type.type, _type.subtype, _type.parameters.copy()
+        elif _type:
+            new_type, new_subtype, new_parameters = cls._parse(_type)
         else:
-            self.type = self.subtype = None
-            self.parameters = {}
-
-            if _type:
-                self._parse(_type)
+            new_type = new_subtype = None
+            new_parameters = {}
 
         if type is not None:
-            self.type = type
+            new_type = type
         if subtype is not None:
-            self.subtype = subtype
+            new_subtype = subtype
         if parameters is not None:
-            self.parameters.update(parameters)
+            new_parameters.update(parameters)
+
+        return super(Type, cls).__new__(cls, new_type, new_subtype, new_parameters)
 
     def __eq__(self, other):
         if isinstance(other, basestring):
             return self.__eq__(self.__class__(other))
 
         if isinstance(other, Type):
-            if self.type != other.type:
-                return False
-            if self.subtype != other.subtype:
-                return False
-            if self.parameters != other.parameters:
-                return False
-            return True
+            return super(Type, self).__eq__(other)
 
         return NotImplemented
 
@@ -65,14 +59,6 @@
             return ret
         return not ret
 
-    def __repr__(self):
-        return '<{0} object: type: {1!r}; subtype: {2!r}; parameters: {3!r}>'.format(
-                self.__class__.__name__,
-                self.type,
-                self.subtype,
-                self.parameters,
-                )
-
     def __unicode__(self):
         ret = [u'{0}/{1}'.format(self.type or '*', self.subtype or '*')]
 
@@ -92,20 +78,24 @@
                 return False
         return True
 
-    def _parse(self, type):
+    @classmethod
+    def _parse(cls, type):
         parts = type.split(';')
 
         type, subtype = parts[0].strip().lower().split('/', 1)
 
-        self.type = type != '*' and type or None
-        self.subtype = subtype != '*' and subtype or None
+        type = type != '*' and type or None
+        subtype = subtype != '*' and subtype or None
+        parameters = {}
 
         for param in parts[1:]:
             key, value = param.strip().split('=', 1)
             # remove quotes
             if value[0] == '"' and value[-1] == '"':
                 value = value[1:-1]
-            self.parameters[key.lower()] = value
+            parameters[key.lower()] = value
+
+        return type, subtype, parameters
 
     def issupertype(self, other):
         """
--- a/docs/admin/configure.rst	Sat Dec 01 15:20:52 2012 +0100
+++ b/docs/admin/configure.rst	Sun Feb 03 03:56:51 2013 +0100
@@ -253,10 +253,9 @@
 
 * to register your image for your email at gravatar.com, you need to give them
   your email address, which is the same as you use in your wiki user profile.
-* we try to avoid exposing the referrer URL to gravatar.com, but this only
-  works if your browser correctly implements rel="noreferrer". If it does not,
-  your wiki item URLs will be exposed, so they will roughly know which people
-  read or work on which wiki items.
+* when the wiki displays an avatar image on some item / view, the URL will be
+  exposed as referrer to the avatar service provider, so they will roughly
+  know which people read or work on which wiki items / views.
 
 XStatic Packages
 ----------------
@@ -631,8 +630,28 @@
 
 Password storage
 ----------------
-Moin never stores passwords in clear text, but always as cryptographic hash
-with a random salt. Currently ssha256 is the default.
+Moin never stores wiki user passwords in clear text, but uses strong
+cryptographic hashes provided by the "passlib" library, see there for details:
+
+    http://packages.python.org/passlib/.
+
+The passlib docs recommend 3 hashing schemes that have good security:
+sha512_crypt, pbkdf2_sha512 and bcrypt (bcrypt has additional binary/compiled
+package requirements, please refer to the passlib docs in case you want to use
+it).
+
+By default, we use sha512_crypt hashes with default parameters as provided
+by passlib (this is same algorithm as moin >= 1.9.7 used by default).
+
+In case you experience slow logins or feel that you might need to tweak the
+hash generation for other reasons, please read the passlib docs. moin allows
+you to configure passlib's CryptContext params within the wiki config, the
+default is this:
+
+::
+    passlib_crypt_context = dict(
+        schemes=["sha512_crypt", ],
+    )
 
 
 Authorization
--- a/docs/admin/upgrade.rst	Sat Dec 01 15:20:52 2012 +0100
+++ b/docs/admin/upgrade.rst	Sun Feb 03 03:56:51 2013 +0100
@@ -19,9 +19,9 @@
 
 From moin < 1.9
 ===============
-If you run an older moin version than 1.9, please first upgrade to moin 1.9.x
-before upgrading to moin2. 
-You may want to run 1.9.x for a while to be sure everything is working as expected.
+If you run an older moin version than 1.9, please first upgrade to a recent
+moin 1.9.x version (preferably >= 1.9.7) before upgrading to moin2.
+You may want to run that for a while to be sure everything is working as expected.
 
 Note: Both moin 1.9.x and moin2 are WSGI applications.
 Upgrading to 1.9 first also makes sense concerning the WSGI / server side.
@@ -29,6 +29,14 @@
 
 From moin 1.9.x
 ===============
+
+If you want to keep your user's password hashes and migrate them to moin2,
+make sure you use moin >= 1.9.7 WITH enabled passlib support and that all
+password hashes stored in user profiles are {PASSLIB} hashes. Other hashes
+will get removed in the migration process and users will need to do password
+recovery via email (or with admin help, if that does not work).
+
+
 Backup
 ------
 Have a backup of everything, so you can go back in case it doesn't do what
@@ -56,6 +64,8 @@
     sitename = u'...' # same as in 1.9
     item_root = u'...' # see page_front_page in 1.9
 
+    # if you had a custom passlib_crypt_context in 1.9, put it here
+
     # configure backend and ACLs to use in future
     # TODO
 
--- a/setup.cfg	Sat Dec 01 15:20:52 2012 +0100
+++ b/setup.cfg	Sun Feb 03 03:56:51 2013 +0100
@@ -45,6 +45,7 @@
 pep8ignore =
  *.py E121 E122 E123 E124 E125 E126 E127 E128  # continuation line indentation
  *.py E225  # missing whitespace around operator
+ *.py E226  # missing optional whitespace around operator
  *.py E261  # less than 2 blanks before inline comment
  *.py E301 E302  # separate toplevel definitions with 2 empty lines, method defs inside class by 1 empty line
  *.py E401  # imports on separate lines
--- a/setup.py	Sat Dec 01 15:20:52 2012 +0100
+++ b/setup.py	Sun Feb 03 03:56:51 2013 +0100
@@ -77,7 +77,7 @@
         'https://bitbucket.org/thomaswaldmann/whoosh/get/2.4x.tar.gz#egg=Whoosh-2.4.99dev',
         # fixed flask-themes, 0.1.3 does not work for flask 0.8.x, thus we use a faked 0.1.3.1:
         'https://bitbucket.org/thomaswaldmann/flask-themes/get/24dcc703953f.tar.gz#egg=Flask-Themes-0.1.3.1',
-        'https://bitbucket.org/thomaswaldmann/emeraldtree/get/tip.tar.gz#egg=emeraldtree-0.9.1',
+        'https://bitbucket.org/thomaswaldmann/emeraldtree/get/tip.tar.gz#egg=emeraldtree-0.9.2',
     ],
     install_requires=[
         'blinker>=1.1', # event signalling (e.g. for change notification trigger)
@@ -88,7 +88,7 @@
         'Flask-Cache>=0.3.4', # caching support
         'Flask-Script>=0.3.3', # scripting support
         'Flask-Themes>=0.1.3.1', # theme support
-        'emeraldtree>=0.9.1', # xml processing
+        'emeraldtree>=0.9.2', # xml processing
         'flatland==dev', # repo checkout at revision 269:6c5d262d7eff works
         'Jinja2>=2.6', # template engine
         'pygments>=1.4', # src code / text file highlighting
@@ -101,6 +101,7 @@
         'whoosh>=2.4.0', # needed for indexed search
         'sphinx>=1.1', # needed to build the docs
         'pdfminer', # pdf -> text/plain conversion
+        'passlib>=1.6.0', # strong password hashing (1.6 needed for consteq)
         'XStatic>=0.0.2', # support for static file pypi packages
         'XStatic-CKEditor>=3.6.1.2',
         'XStatic-jQuery>=1.8.2',