changeset 2398:f9d087b59fd4

Merge devel.
author Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl>
date Sun, 15 Jul 2007 23:41:39 +0200
parents 75817589c437 (diff) a10ae31a6133 (current diff)
children 245181ec9fe2
files
diffstat 24 files changed, 1027 insertions(+), 184 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/action/newaccount.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/action/newaccount.py	Sun Jul 15 23:41:39 2007 +0200
@@ -10,7 +10,6 @@
 from MoinMoin.Page import Page
 from MoinMoin.widget import html
 import MoinMoin.events as events
-from MoinMoin.userprefs.prefs import Settings
 
 
 _debug = False
@@ -90,6 +89,59 @@
     return result
 
 
+def _create_form(request):
+    _ = request.getText
+    url = request.page.url(request)
+    ret = html.FORM(action=url)
+    ret.append(html.INPUT(type='hidden', name='action', value='newaccount'))
+    lang_attr = request.theme.ui_lang_attr()
+    ret.append(html.Raw('<div class="userpref"%s>' % lang_attr))
+    tbl = html.TABLE(border="0")
+    ret.append(tbl)
+    ret.append(html.Raw('</div>'))
+
+    row = html.TR()
+    tbl.append(row)
+    row.append(html.TD().append(html.STRONG().append(
+                                  html.Text(_("Name")))))
+    row.append(html.TD().append(html.INPUT(type="text", size="36",
+                                           name="name")))
+
+    row = html.TR()
+    tbl.append(row)
+    row.append(html.TD().append(html.STRONG().append(
+                                  html.Text(_("Password")))))
+    row.append(html.TD().append(html.INPUT(type="password", size="36",
+                                           name="password")))
+
+    row = html.TR()
+    tbl.append(row)
+    row.append(html.TD().append(html.STRONG().append(
+                                  html.Text(_("Password repeat")))))
+    row.append(html.TD().append(html.INPUT(type="password", size="36",
+                                           name="password2")))
+
+    row = html.TR()
+    tbl.append(row)
+    row.append(html.TD().append(html.STRONG().append(html.Text(_("Email")))))
+    row.append(html.TD().append(html.INPUT(type="text", size="36",
+                                           name="email")))
+
+    row = html.TR()
+    tbl.append(row)
+    row.append(html.TD())
+    td = html.TD()
+    row.append(td)
+    td.append(html.INPUT(type="submit", name="create_only",
+                         value=_('Create Profile')))
+    if request.cfg.mail_enabled:
+        td.append(html.Text(' '))
+        td.append(html.INPUT(type="submit", name="create_and_mail",
+                             value="%s + %s" % (_('Create Profile'),
+                                                _('Email'))))
+
+    return unicode(ret)
+
 def execute(pagename, request):
     pagename = pagename
     page = Page(request, pagename)
@@ -108,7 +160,7 @@
         request.write(request.formatter.startContent("content"))
 
         # THIS IS A BIG HACK. IT NEEDS TO BE CLEANED UP
-        request.write(Settings(request).create_form(create_only=True))
+        request.write(_create_form(request))
 
         request.write(request.formatter.endContent())
 
--- a/MoinMoin/action/recoverpass.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/action/recoverpass.py	Sun Jul 15 23:41:39 2007 +0200
@@ -9,7 +9,6 @@
 from MoinMoin import user, wikiutil
 from MoinMoin.Page import Page
 from MoinMoin.widget import html
-from MoinMoin.userprefs.prefs import Settings
 
 def _do_recover(request):
     _ = request.getText
@@ -29,6 +28,33 @@
 
     return _("Found no account matching the given email address '%(email)s'!") % {'email': email}
 
+def _create_form(request):
+    _ = request.getText
+    url = request.page.url(request)
+    ret = html.FORM(action=url)
+    ret.append(html.INPUT(type='hidden', name='action', value='recoverpass'))
+    lang_attr = request.theme.ui_lang_attr()
+    ret.append(html.Raw('<div class="userpref"%s>' % lang_attr))
+    tbl = html.TABLE(border="0")
+    ret.append(tbl)
+    ret.append(html.Raw('</div>'))
+
+    row = html.TR()
+    tbl.append(row)
+    row.append(html.TD().append(html.STRONG().append(html.Text(_("Email")))))
+    row.append(html.TD().append(html.INPUT(type="text", size="36",
+                                           name="email")))
+
+    row = html.TR()
+    tbl.append(row)
+    row.append(html.TD())
+    td = html.TD()
+    row.append(td)
+    td.append(html.INPUT(type="submit", name="account_sendmail",
+                         value=_('Mail me my account data')))
+
+    return unicode(ret)
+
 
 def execute(pagename, request):
     pagename = pagename
@@ -51,8 +77,7 @@
             request.write(_("""This wiki is not enabled for mail processing.
 Contact the owner of the wiki, who can enable email."""))
         else:
-            # THIS IS A BIG HACK. IT NEEDS TO BE CLEANED UP
-            request.write(Settings(request).create_form(recover_only=True))
+            request.write(_create_form(request))
 
             request.write(_("""
 == Recovering a lost password ==
--- a/MoinMoin/action/userprefs.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/action/userprefs.py	Sun Jul 15 23:41:39 2007 +0200
@@ -20,15 +20,20 @@
     """
     _ = request.getText
     sub = request.form.get('handler', [None])[0]
+
+    if sub in request.cfg.disabled_userprefs:
+        return None
+
     try:
         cls = wikiutil.importPlugin(request.cfg, 'userprefs', sub, 'Settings')
     except wikiutil.PluginMissingError:
-        return _("No such preferences plugin")
+        # we never show this plugin to click on so no need to
+        # give a message here
+        return None
 
     obj = cls(request)
     if not obj.allowed():
-        # intentionally do not let the user know this exists
-        return _("No such preferences plugin")
+        return None
     return obj.handle_form()
 
 def _create_prefs_page(request, sel=None):
@@ -40,6 +45,8 @@
     items = html.UL()
     ret.append(items)
     for sub in plugins:
+        if sub in request.cfg.disabled_userprefs:
+            continue
         cls = wikiutil.importPlugin(request.cfg, 'userprefs', sub, 'Settings')
         obj = cls(request)
         if not obj.allowed():
@@ -55,16 +62,25 @@
     pagename = request.page.page_name
 
     if 'handler' in request.form:
-        return _create_prefs_page(request), None, _handle_submission(request)
+        msg = _handle_submission(request)
+    else:
+        msg = None
 
     sub = request.form.get('sub', [''])[0]
-    try:
-        cls = wikiutil.importPlugin(request.cfg, 'userprefs', sub, 'Settings')
-    except wikiutil.PluginMissingError:
-        return _create_prefs_page(request), None, None
+    cls = None
+    if sub and not sub in request.cfg.disabled_userprefs:
+        try:
+            cls = wikiutil.importPlugin(request.cfg, 'userprefs', sub, 'Settings')
+        except wikiutil.PluginMissingError:
+            # cls is already None
+            pass
 
-    obj = cls(request)
-    return obj.create_form(), obj.title, None
+    obj = cls and cls(request)
+
+    if not obj or not obj.allowed():
+        return _create_prefs_page(request), None, msg
+
+    return obj.create_form(), obj.title, msg
 
 
 def execute(pagename, request):
--- a/MoinMoin/auth/__init__.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/auth/__init__.py	Sun Jul 15 23:41:39 2007 +0200
@@ -3,10 +3,11 @@
     MoinMoin - modular authentication handling
 
     Each authentication method is an object instance containing
-    three methods:
+    four methods:
       * login(request, user_obj, **kw)
       * logout(request, user_obj, **kw)
       * request(request, user_obj, **kw)
+      * login_hint(request)
 
     The kw arguments that are passed in are currently:
        attended: boolean indicating whether a user (attended=True) or
@@ -20,6 +21,12 @@
                that the browser sent
        multistage: boolean indicating multistage login continuation
                    [may not be present, login only]
+       openid_identifier: the OpenID identifier we got from the form
+                          (or None) [login only]
+
+    login_hint() should return a HTML text that is displayed to the user right
+    below the login form, it should tell the user what to do in case of a
+    forgotten password and how to create an account (if applicable.)
 
     More may be added.
 
@@ -85,6 +92,7 @@
      * login_inputs: a list of required inputs, currently supported are
                       - 'username': username entry field
                       - 'password': password entry field
+                      - 'openid_identifier': OpenID entry field
      * logout_possible: boolean indicating whether this auth methods
                         supports logging out
      * name: name of the auth method, must be the same as given as the
@@ -184,6 +192,8 @@
         if self.name and user_obj and user_obj.auth_method == self.name:
             user_obj.valid = False
         return user_obj, True
+    def login_hint(self, request):
+        return None
 
 class MoinLogin(BaseAuth):
     """ handle login from moin login form """
@@ -222,3 +232,12 @@
         else:
             if verbose: request.log("moin_login not valid, previous valid=%d." % user_obj.valid)
             return ContinueLogin(user_obj, _("Invalid username or password."))
+
+    def login_hint(self, request):
+        _ = request.getText
+        userprefslink = request.page.url(request, querystr={'action': 'newaccount'})
+        sendmypasswordlink = request.page.url(request, querystr={'action': 'recoverpass'})
+        return _('If you do not have an account, <a href="%(userprefslink)s">you can create one now</a>. '
+                 '<a href="%(sendmypasswordlink)s">Forgot your password?</a>', formatted=False) % {
+               'userprefslink': userprefslink,
+               'sendmypasswordlink': sendmypasswordlink}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/botbouncer.py	Sun Jul 15 23:41:39 2007 +0200
@@ -0,0 +1,63 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - botbouncer.com verifier for OpenID login
+
+    @copyright: 2007 MoinMoin:JohannesBerg
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin import user
+from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin, MultistageRedirectLogin
+from MoinMoin import wikiutil
+from MoinMoin.widget import html
+from urllib import urlopen, quote_plus
+
+class BotBouncer(BaseAuth):
+    name = 'botbouncer'
+
+    def __init__(self, apikey):
+        BaseAuth.__init__(self)
+        self.apikey = apikey
+
+    def login(self, request, user_obj, **kw):
+        if kw.get('multistage'):
+            uid = request.session.get('botbouncer.uid', None)
+            if not uid:
+                return CancelLogin()
+            openid = request.session['botbouncer.id']
+            del request.session['botbouncer.id']
+            del request.session['botbouncer.uid']
+            user_obj = user.User(request, uid, auth_method='openid',
+                                 auth_username=openid)
+
+        if not user_obj or not user_obj.valid:
+            return ContinueLogin(user_obj)
+
+        if user_obj.auth_method != 'openid':
+            return ContinueLogin(user_obj)
+
+        openid_id = user_obj.auth_username
+
+        _ = request.getText
+
+        try:
+            url = "http://botbouncer.com/api/info?openid=%s&api_key=%s" % (
+                           quote_plus(openid_id), self.apikey)
+            data = urlopen(url).read().strip()
+        except IOError:
+            return CancelLogin(_('Could not contact botbouncer.com.'))
+
+        data = data.split(':')
+        if len(data) != 2 or data[0] != 'verified':
+            return CancelLogin('botbouncer.com verification failed, probably invalid API key.')
+
+        if data[1].lower() == 'true':
+            # they proved they are human already
+            return ContinueLogin(user_obj)
+
+        # tell them to verify at bot bouncer first
+        request.session['botbouncer.id'] = openid_id
+        request.session['botbouncer.uid'] = user_obj.id
+
+        goto = "http://botbouncer.com/captcha/queryuser?return_to=%%return_form&openid=%s" % (
+            quote_plus(request.session['botbouncer.id']))
+        return MultistageRedirectLogin(goto)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/openidrp.py	Sun Jul 15 23:41:39 2007 +0200
@@ -0,0 +1,240 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - OpenID authorization
+
+    @copyright: 2007 MoinMoin:JohannesBerg
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.util.moinoid import MoinOpenIDStore
+from MoinMoin import user
+from MoinMoin.auth import BaseAuth
+from openid.consumer import consumer
+from openid.yadis.discover import DiscoveryFailure
+from openid.fetchers import HTTPFetchingError
+from MoinMoin import wikiutil
+from MoinMoin.widget import html
+from MoinMoin.auth import (CancelLogin, ContinueLogin, MultistageFormLogin,
+    MultistageRedirectLogin, get_multistage_continuation_url)
+
+class OpenIDAuth(BaseAuth):
+    login_inputs = ['openid_identifier']
+    name = 'openid'
+    logout_possible = True
+
+    def _get_account_name(self, request, form, msg=None):
+        # now we need to ask the user for a new username
+        # that they want to use on this wiki
+        # XXX: request nickname from OP and suggest using it
+        # (if it isn't in use yet)
+        _ = request.getText
+        form.append(html.INPUT(type='hidden', name='oidstage', value='2'))
+        table = html.TABLE(border='0')
+        form.append(table)
+        td = html.TD(colspan=2)
+        td.append(html.Raw(_("""Please choose an account name now.
+If you choose an existing account name you will be asked for the
+password and be able to associate the account with your OpenID.""")))
+        table.append(html.TR().append(td))
+        if msg:
+            td = html.TD(colspan='2')
+            td.append(html.P().append(html.STRONG().append(html.Raw(msg))))
+            table.append(html.TR().append(td))
+        td1 = html.TD()
+        td1.append(html.STRONG().append(html.Raw(_('Name'))))
+        td2 = html.TD()
+        td2.append(html.INPUT(type='text', name='username'))
+        table.append(html.TR().append(td1).append(td2))
+        td1 = html.TD()
+        td2 = html.TD()
+        td2.append(html.INPUT(type='submit', name='submit',
+                              value=_('Choose this name')))
+        table.append(html.TR().append(td1).append(td2))
+
+    def _associate_account(self, request, form, accountname, msg=None):
+        _ = request.getText
+
+        form.append(html.INPUT(type='hidden', name='oidstage', value='3'))
+        table = html.TABLE(border='0')
+        form.append(table)
+        td = html.TD(colspan=2)
+        td.append(html.Raw(_("""The username you have chosen is already
+taken. If it is your username, enter your password below to associate
+the username with your OpenID. Otherwise, please choose a different
+username and leave the password field blank.""")))
+        table.append(html.TR().append(td))
+        if msg:
+            td.append(html.P().append(html.STRONG().append(html.Raw(msg))))
+        td1 = html.TD()
+        td1.append(html.STRONG().append(html.Raw(_('Name'))))
+        td2 = html.TD()
+        td2.append(html.INPUT(type='text', name='username', value=accountname))
+        table.append(html.TR().append(td1).append(td2))
+        td1 = html.TD()
+        td1.append(html.STRONG().append(html.Raw(_('Password'))))
+        td2 = html.TD()
+        td2.append(html.INPUT(type='password', name='password'))
+        table.append(html.TR().append(td1).append(td2))
+        td1 = html.TD()
+        td2 = html.TD()
+        td2.append(html.INPUT(type='submit', name='submit',
+                              value=_('Associate this name')))
+        table.append(html.TR().append(td1).append(td2))
+
+    def _handle_verify_continuation(self, request):
+        _ = request.getText
+        oidconsumer = consumer.Consumer(request.session,
+                                        MoinOpenIDStore(request))
+        query = {}
+        for key in request.form:
+            query[key] = request.form[key][0]
+        return_to = get_multistage_continuation_url(request, self.name,
+                                                    {'oidstage': '1'})
+        info = oidconsumer.complete(query, return_to=return_to)
+        if info.status == consumer.FAILURE:
+            return CancelLogin(_('OpenID error: %s.') % info.message)
+        elif info.status == consumer.CANCEL:
+            return CancelLogin(_('Verification canceled.'))
+        elif info.status == consumer.SUCCESS:
+            # try to find user object
+            uid = user.getUserIdByOpenId(request, info.identity_url)
+            if uid:
+                u = user.User(request, id=uid, auth_method=self.name,
+                              auth_username=info.identity_url)
+                return ContinueLogin(u)
+            # if no user found, then we need to ask for a username,
+            # possibly associating an existing account.
+            request.session['openid.id'] = info.identity_url
+            return MultistageFormLogin(self._get_account_name)
+        else:
+            return CancelLogin(_('OpenID failure.'))
+
+    def _handle_name_continuation(self, request):
+        if not 'openid.id' in request.session:
+            return CancelLogin(None)
+
+        _ = request.getText
+        newname = request.form.get('username', [''])[0]
+        if not newname:
+            return MultistageFormLogin(self._get_account_name)
+        if not user.isValidName(request, newname):
+            return MultistageFormLogin(self._get_account_name,
+                    _('This is not a valid username, choose a different one.'))
+        uid = None
+        if newname:
+            uid = user.getUserId(request, newname)
+        if not uid:
+            # we can create a new user with this name :)
+            u = user.User(request, id=uid, auth_method=self.name,
+                          auth_username=request.session['openid.id'])
+            u.name = newname
+            u.openids = [request.session['openid.id']]
+            u.aliasname = request.session['openid.id']
+            del request.session['openid.id']
+            u.save()
+            return ContinueLogin(u)
+        # requested username already exists. if they know the password,
+        # they can associate that account with the openid.
+        assoc = lambda req, form: self._associate_account(req, form, newname)
+        return MultistageFormLogin(assoc)
+
+    def _handle_associate_continuation(self, request):
+        if not 'openid.id' in request.session:
+            return CancelLogin()
+
+        _ = request.getText
+        username = request.form.get('username', [''])[0]
+        password = request.form.get('password', [''])[0]
+        if not password:
+            return self._handle_name_continuation(request)
+        u = user.User(request, name=username, password=password,
+                      auth_method=self.name,
+                      auth_username=request.session['openid.id'])
+        if u.valid:
+            if not hasattr(u, 'openids'):
+                u.openids = []
+            u.openids.append(request.session['openid.id'])
+            if not u.aliasname:
+                u.aliasname = request.session['openid.id']
+            u.save()
+            del request.session['openid.id']
+            return ContinueLogin(u, _('Your account is now associated to your OpenID.'))
+        else:
+            msg = _('The password you entered is not valid.')
+            assoc = lambda req, form: self._associate_account(req, form, username, msg=msg)
+            return MultistageFormLogin(assoc)
+
+    def _handle_continuation(self, request):
+        oidstage = request.form.get('oidstage', [0])[0]
+        if oidstage == '1':
+            return self._handle_verify_continuation(request)
+        elif oidstage == '2':
+            return self._handle_name_continuation(request)
+        elif oidstage == '3':
+            return self._handle_associate_continuation(request)
+        return CancelLogin()
+
+    def _openid_form(self, request, form, oidhtml):
+        _ = request.getText
+        txt = _('OpenID verification requires that you click this button:')
+        # create JS to automatically submit the form if possible
+        submitjs = """<script type="text/javascript">
+<!--//
+document.getElementById("openid_message").submit();
+//-->
+</script>
+"""
+        return ''.join([txt, oidhtml, submitjs])
+
+    def login(self, request, user_obj, **kw):
+        continuation = kw.get('multistage')
+
+        if continuation:
+            return self._handle_continuation(request)
+
+        # openid is designed to work together with other auths
+        if user_obj and user_obj.valid:
+            return ContinueLogin(user_obj)
+
+        openid_id = kw.get('openid_identifier')
+        # nothing entered? continue...
+        if not openid_id:
+            return ContinueLogin(user_obj)
+
+        _ = request.getText
+
+        # user entered something but the session can't be stored
+        if not request.session.is_stored:
+            return ContinueLogin(user_obj,
+                                 _('Anonymous sessions need to be enabled for OpenID login.'))
+
+        oidconsumer = consumer.Consumer(request.session,
+                                        MoinOpenIDStore(request))
+
+        try:
+            oidreq = oidconsumer.begin(openid_id)
+        except HTTPFetchingError:
+            return ContinueLogin(None, _('Failed to resolve OpenID.'))
+        except DiscoveryFailure:
+            return ContinueLogin(None, _('OpenID discovery failure, not a valid OpenID.'))
+        else:
+            if oidreq is None:
+                return ContinueLogin(None, _('No OpenID.'))
+
+            return_to = get_multistage_continuation_url(request, self.name,
+                                                        {'oidstage': '1'})
+            trust_root = request.getBaseURL()
+            if oidreq.shouldSendRedirect():
+                redirect_url = oidreq.redirectURL(trust_root, return_to)
+                return MultistageRedirectLogin(redirect_url)
+            else:
+                form_html = oidreq.formMarkup(trust_root, return_to,
+                    form_tag_attrs={'id': 'openid_message'})
+                mcall = lambda request, form:\
+                    self._openid_form(request, form, form_html)
+                ret = MultistageFormLogin(mcall)
+                return ret
+
+    def login_hint(self, request):
+        _ = request.getText
+        return _("If you do not have an account yet, you can still log in "
+                 "with your OpenID and create one during login.")
--- a/MoinMoin/config/multiconfig.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/config/multiconfig.py	Sun Jul 15 23:41:39 2007 +0200
@@ -516,6 +516,9 @@
 
     url_mappings = {}
 
+    # allow disabling certain userpreferences plugins
+    disabled_userprefs = []
+
     user_checkbox_fields = [
         ('mailto_author', lambda _: _('Publish my email (not my wiki homepage) in author info')),
         ('edit_on_doubleclick', lambda _: _('Open editor on double click')),
@@ -562,8 +565,6 @@
     user_form_fields = [
         ('name', _('Name'), "text", "36", _("(Use Firstname''''''Lastname)")),
         ('aliasname', _('Alias-Name'), "text", "36", ''),
-        ('password', _('Password'), "password", "36", ''),
-        ('password2', _('Password repeat'), "password", "36", _('(Only for password change or new account)')),
         ('email', _('Email'), "text", "36", ''),
         ('jid', _('Jabber ID'), "text", "36", ''),
         ('css_url', _('User CSS URL'), "text", "40", _('(Leave it empty for disabling user CSS)')),
--- a/MoinMoin/parser/_tests/test_text_moin_wiki.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/parser/_tests/test_text_moin_wiki.py	Sun Jul 15 23:41:39 2007 +0200
@@ -573,3 +573,24 @@
         assert expected == result
 
 
+class TestLinkingMarkup(ParserTestCase):
+    """ Test wiki markup """
+
+    text = 'AAA %s AAA'
+    needle = re.compile(text % r'(.+)')
+    _tests = (
+        # test,                       expected
+        ('["something"]',             '<a class="nonexistent" href="./something">something</a>'),
+        ("['something']",             "['something']"),
+        ('MoinMoin:"something"',      '<a class="interwiki" href="http://moinmoin.wikiwikiweb.de/something" title="MoinMoin">something</a>'),
+        ('MoinMoin:"with space"',     '<a class="interwiki" href="http://moinmoin.wikiwikiweb.de/with%20space" title="MoinMoin">with space</a>'),
+        ('RFC:"1 2 3"',               '<a class="interwiki" href="http://www.ietf.org/rfc/rfc1%202%203" title="RFC">1 2 3</a>'),
+        ("RFC:'something else'",      "RFC:'something else'"),
+        )
+
+    def testTextFormating(self):
+        """ parser.wiki: text formating """
+        for test, expected in self._tests:
+            html = self.parse(self.text % test)
+            result = self.needle.search(html).group(1)
+            assert result == expected
--- a/MoinMoin/parser/text_moin_wiki.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/parser/text_moin_wiki.py	Sun Jul 15 23:41:39 2007 +0200
@@ -91,7 +91,7 @@
 (?P<tableZ>\|\| $)
 (?P<table>(?:\|\|)+(?:<[^>]*?>)?(?!\|? $))
 (?P<heading>^\s*(?P<hmarker>=+)\s.*\s(?P=hmarker) $)
-(?P<interwiki>[A-Z][a-zA-Z]+\:(%(q_string)s|([^\s'\"\:\<\|]([^\s%(punct)s]|([%(punct)s][^\s%(punct)s]))+)))
+(?P<interwiki>[A-Z][a-zA-Z]+\:(%(dq_string)s|([^\s'\"\:\<\|]([^\s%(punct)s]|([%(punct)s][^\s%(punct)s]))+)))
 (?P<word>%(word_rule)s)
 (?P<url_bracket>\[((%(url)s)\:|#|\:)[^\s\]]+(\s[^\]]+)?\])
 (?P<url>%(url_rule)s)
@@ -100,12 +100,12 @@
 (?P<smileyA>^(%(smiley)s)(?=\s))
 (?P<ent_symbolic>&[a-zA-Z]+;)
 (?P<ent>[<>&])
-(?P<wikiname_bracket>\[%(q_string)s.*?\])
+(?P<wikiname_bracket>\[%(dq_string)s.*?\])
 (?P<tt_bt>`.*?`)"""  % {
 
         'url': url_pattern,
         'punct': punct_pattern,
-        'q_string': q_string,
+        'dq_string': dq_string,
         'ol_rule': ol_rule,
         'dl_rule': dl_rule,
         'url_rule': url_rule,
--- a/MoinMoin/request/__init__.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/request/__init__.py	Sun Jul 15 23:41:39 2007 +0200
@@ -240,7 +240,9 @@
                 self._setuid_real_user = self.user
                 uid = self.session['setuid']
                 self.user = user.User(self, uid, auth_method='setuid')
-                self.user.disabled = False
+                # set valid to True so superusers can even switch
+                # to disable accounts
+                self.user.valid = True
 
             if self.action != 'xmlrpc':
                 if not self.forbidden and self.isForbidden():
@@ -603,16 +605,18 @@
     def _handle_auth_form(self, user_obj):
         username = self.form.get('name', [None])[0]
         password = self.form.get('password', [None])[0]
+        oid = self.form.get('openid_identifier', [None])[0]
         login = 'login' in self.form
         logout = 'logout' in self.form
         stage = self.form.get('stage', [None])[0]
         return self.handle_auth(user_obj, attended=True, username=username,
                                 password=password, login=login, logout=logout,
-                                stage=stage)
+                                stage=stage, openid_identifier=oid)
 
     def handle_auth(self, user_obj, attended=False, **kw):
         username = kw.get('username')
         password = kw.get('password')
+        oid = kw.get('openid_identifier')
         login = kw.get('login')
         logout = kw.get('logout')
         stage = kw.get('stage')
@@ -623,6 +627,7 @@
             extra['attended'] = attended
             extra['username'] = username
             extra['password'] = password
+            extra['openid_identifier'] = oid
             if stage:
                 extra['multistage'] = True
         login_msgs = []
--- a/MoinMoin/user.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/user.py	Sun Jul 15 23:41:39 2007 +0200
@@ -88,7 +88,11 @@
             u = User(request, id=userid)
             if hasattr(u, key):
                 value = getattr(u, key)
-                _key2id[value] = userid
+                if isinstance(value, list):
+                    for val in value:
+                        _key2id[val] = userid
+                else:
+                    _key2id[value] = userid
         arena = 'user'
         cache = caching.CacheEntry(request, arena, cachekey, scope='wiki', use_pickle=True)
         try:
@@ -109,6 +113,16 @@
     return _getUserIdByKey(request, 'name', searchName)
 
 
+def getUserIdByOpenId(request, openid):
+    """ Get the user ID for a specific OpenID.
+
+    @param openid: the openid to look up
+    @rtype: string
+    @return: the corresponding user ID or None
+    """
+    return _getUserIdByKey(request, 'openids', openid)
+
+
 def getUserIdentification(request, username=None):
     """ Return user name or IP or '<unknown>' indicator.
 
--- a/MoinMoin/userform/login.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/userform/login.py	Sun Jul 15 23:41:39 2007 +0200
@@ -37,12 +37,11 @@
         sn = request.getScriptname()
         pi = request.getPathinfo()
         action = u"%s%s" % (sn, pi)
-        userprefslink = request.page.url(request, querystr={'action': 'newaccount'})
-        sendmypasswordlink = request.page.url(request, querystr={'action': 'recoverpass'})
-        hint = _('If you do not have an account, <a href="%(userprefslink)s">you can create one now</a>. '
-                 '<a href="%(sendmypasswordlink)s">Forgot your password?</a>', formatted=False) % {
-                 'userprefslink': userprefslink,
-                 'sendmypasswordlink': sendmypasswordlink}
+        hints = []
+        for authm in request.cfg.auth:
+            hint = authm.login_hint(request)
+            if hint:
+                hints.append(hint)
         self._form = html.FORM(action=action, name="loginform")
         self._table = html.TABLE(border="0")
 
@@ -52,7 +51,8 @@
 
         self._form.append(html.INPUT(type="hidden", name="action", value="login"))
         self._form.append(self._table)
-        self._form.append(html.P().append(html.Raw(hint)))
+        for hint in hints:
+            self._form.append(html.P().append(html.Raw(hint)))
         self._form.append(html.Raw("</div>"))
 
         cfg = request.cfg
@@ -70,6 +70,14 @@
                 ),
             ])
 
+        if 'openid_identifier' in cfg.auth_login_inputs:
+            self.make_row(_('OpenID'), [
+                html.INPUT(
+                    type="text", size="32", name="openid_identifier",
+                    id="openididentifier"
+                ),
+            ])
+
         self.make_row('', [
             html.INPUT(
                 type="submit", name='login', value=_('Login')
--- a/MoinMoin/userprefs/__init__.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/userprefs/__init__.py	Sun Jul 15 23:41:39 2007 +0200
@@ -8,6 +8,7 @@
     @license: GNU GPL, see COPYING for details.
 """
 from MoinMoin.util import pysupport
+from MoinMoin.widget import html
 
 
 # create a list of extension actions from the package directory
@@ -30,7 +31,8 @@
         '''
         self.request = request
         self._ = request.getText
-        title = 'No name set'
+        self.name = None
+        self.title = 'No name set'
 
     def create_form(self):
         '''
@@ -39,6 +41,9 @@
             hidden fields
               * action: set to "userprefs"
               * handler: set to the plugin name
+            It can additionally contain the hidden field
+            'sub' set to the plugin name if the plugin needs
+            multiple forms (wizard-like.)
         '''
         raise NotImplementedError
 
@@ -59,4 +64,39 @@
             title should be listed or not and whether
             submissions are accepted.
         '''
-        return True
+        return self.request.user and self.request.user.valid
+
+    def make_form(self, explanation=None):
+        '''
+            To have a consistent UI, use this method for most
+            preferences forms and then call make_row(). See
+            existing plugins, e.g. changepass.py.
+        '''
+        action = self.request.page.url(self.request)
+        _form = html.FORM(action=action)
+        _form.append(html.INPUT(type="hidden", name="action", value="userprefs"))
+        _form.append(html.INPUT(type="hidden", name="handler", value=self.name))
+
+        self._table = html.TABLE(border="0")
+
+        # Use the user interface language and direction
+        lang_attr = self.request.theme.ui_lang_attr()
+        _form.append(html.Raw('<div class="userpref"%s>' % lang_attr))
+        para = html.P()
+        _form.append(para)
+        if explanation:
+            para.append(explanation)
+
+        para.append(self._table)
+        _form.append(html.Raw("</div>"))
+
+        return _form
+
+    def make_row(self, label, cell, **kw):
+        '''
+           Create a row in the form table.
+        '''
+        self._table.append(html.TR().extend([
+            html.TD(**kw).extend([html.B().append(label), '   ']),
+            html.TD().extend(cell),
+        ]))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/userprefs/changepass.py	Sun Jul 15 23:41:39 2007 +0200
@@ -0,0 +1,82 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Password change preferences plugin
+
+    @copyright: 2001-2004 Juergen Hermann <jh@web.de>,
+                2003-2007 MoinMoin:ThomasWaldmann
+                2007      MoinMoin:JohannesBerg
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import time
+from MoinMoin import user, wikiutil
+from MoinMoin.widget import html
+from MoinMoin.userprefs import UserPrefBase
+
+
+class Settings(UserPrefBase):
+    def __init__(self, request):
+        """ Initialize password change form. """
+        UserPrefBase.__init__(self, request)
+        self.request = request
+        self._ = request.getText
+        _ = request.getText
+        self.cfg = request.cfg
+        self.title = _("Change password")
+        self.name = 'changepass'
+
+
+    def allowed(self):
+        return (not 'password' in self.cfg.user_form_remove and
+                not 'password' in self.cfg.user_form_disable and
+                UserPrefBase.allowed(self) and
+                not 'password' in self.request.user.auth_attribs)
+
+
+    def handle_form(self):
+        _ = self._
+        form = self.request.form
+
+        if form.has_key('cancel'):
+            return
+
+        if self.request.request_method != 'POST':
+            return
+
+        password = form.get('password', [''])[0]
+        password2 = form.get('password2', [''])[0]
+
+        # Check if password is given and matches with password repeat
+        if password != password2:
+            return _("Passwords don't match!")
+
+        # Encode password
+        if password and not password.startswith('{SHA}'):
+            try:
+                self.request.user.enc_password = user.encodePassword(password)
+                self.request.user.save()
+                return _("Your password has been changed.")
+            except UnicodeError, err:
+                # Should never happen
+                return "Can't encode password: %s" % str(err)
+
+
+    def create_form(self, create_only=False, recover_only=False):
+        """ Create the complete HTML form code. """
+        _ = self._
+        form = self.make_form(html.Text(_("To change your password, "
+                                          "enter a new password twice.")))
+
+        self.make_row(_('Password'),
+                      [html.INPUT(type="password", size=36, name="password")])
+        self.make_row(_('Password repeat'),
+                      [html.INPUT(type="password", size=36, name="password2")])
+
+        # Add buttons
+        self.make_row('', [
+                html.INPUT(type="submit", name='save', value=_("Change password")),
+                ' ',
+                html.INPUT(type="submit", name='cancel', value=_("Cancel")),
+              ])
+
+        return unicode(form)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/userprefs/oid.py	Sun Jul 15 23:41:39 2007 +0200
@@ -0,0 +1,228 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - OpenID preferences
+
+    @copyright: 2007     MoinMoin:JohannesBerg
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import wikiutil, user
+from MoinMoin.widget import html
+from MoinMoin.userprefs import UserPrefBase
+import sha
+try:
+    from MoinMoin.auth.openidrp import OpenIDAuth
+    from MoinMoin.util.moinoid import MoinOpenIDStore
+    from openid.consumer import consumer
+    from openid.yadis.discover import DiscoveryFailure
+    from openid.fetchers import HTTPFetchingError
+    _openid_disabled = False
+except ImportError:
+    _openid_disabled = True
+
+
+class Settings(UserPrefBase):
+    def __init__(self, request):
+        """ Initialize OpenID settings form. """
+        UserPrefBase.__init__(self, request)
+        self.request = request
+        self._ = request.getText
+        self.cfg = request.cfg
+        self.title = self._("OpenID settings")
+
+    def allowed(self):
+        if _openid_disabled:
+            return False
+        for authm in self.request.cfg.auth:
+            if isinstance(authm, OpenIDAuth):
+                return UserPrefBase.allowed(self)
+        return False
+
+    def _handle_remove(self):
+        _ = self.request.getText
+        if not hasattr(self.request.user, 'openids'):
+            return
+        openids = self.request.user.openids[:]
+        for oid in self.request.user.openids:
+            name = "rm-%s" % sha.new(oid).hexdigest()
+            if name in self.request.form:
+                openids.remove(oid)
+        if not openids and len(self.request.cfg.auth) == 1:
+            return _("Cannot remove all OpenIDs.")
+        self.request.user.openids = openids
+        self.request.user.save()
+        return _("The selected OpenIDs have been removed.")
+
+    def _handle_add(self):
+        _ = self.request.getText
+        request = self.request
+
+        openid_id = request.form.get('openid_identifier', [''])[0]
+        if not openid_id:
+            return _("No OpenID.")
+
+        if (hasattr(self.request.user, 'openids') and
+            openid_id in request.user.openids):
+            return _("OpenID is already present.")
+
+        oidconsumer = consumer.Consumer(request.session,
+                                        MoinOpenIDStore(self.request))
+        try:
+            oidreq = oidconsumer.begin(openid_id)
+        except HTTPFetchingError:
+            return _('Failed to resolve OpenID.')
+        except DiscoveryFailure:
+            return _('OpenID discovery failure, not a valid OpenID.')
+        else:
+            if oidreq is None:
+                return _("No OpenID.")
+
+            qstr = wikiutil.makeQueryString({'action': 'userprefs',
+                                             'handler': 'oid',
+                                             'oid.return': '1'})
+            return_to = '%s/%s' % (request.getBaseURL(),
+                                   request.page.url(request, qstr))
+            trust_root = request.getBaseURL()
+            if oidreq.shouldSendRedirect():
+                redirect_url = oidreq.redirectURL(trust_root, return_to)
+                request.http_redirect(redirect_url)
+            else:
+                form_html = oidreq.formMarkup(trust_root, return_to,
+                    form_tag_attrs={'id': 'openid_message'})
+                request.session['openid.prefs.form_html'] = form_html
+
+
+    def _handle_oidreturn(self):
+        request = self.request
+        _ = request.getText
+
+        oidconsumer = consumer.Consumer(request.session,
+                                        MoinOpenIDStore(request))
+        query = {}
+        for key in request.form:
+            query[key] = request.form[key][0]
+        qstr = wikiutil.makeQueryString({'action': 'userprefs',
+                                         'handler': 'oid',
+                                         'oid.return': '1'})
+        return_to = '%s/%s' % (request.getBaseURL(),
+                               request.page.url(request, qstr))
+        info = oidconsumer.complete(query, return_to=return_to)
+        if info.status == consumer.FAILURE:
+            return _('OpenID error: %s.') % info.message
+        elif info.status == consumer.CANCEL:
+            return _('Verification canceled.')
+        elif info.status == consumer.SUCCESS:
+            if not hasattr(self.request.user, 'openids'):
+                request.user.openids = []
+
+            if info.identity_url in request.user.openids:
+                return _("OpenID is already present.")
+
+            if user.getUserIdByOpenId(request, info.identity_url):
+                return _("This OpenID is already used for another account.")
+
+            # all fine
+            request.user.openids.append(info.identity_url)
+            request.user.save()
+            return _("OpenID added successfully.")
+        else:
+            return _('OpenID failure.')
+
+
+    def handle_form(self):
+        _ = self._
+        form = self.request.form
+
+        if form.has_key('oid.return'):
+            return self._handle_oidreturn()
+
+        if form.has_key('cancel'):
+            return
+
+        if form.has_key('remove'):
+            return self._handle_remove()
+
+        if form.has_key('add'):
+            return self._handle_add()
+
+        return
+
+    def _make_form(self):
+        sn = self.request.getScriptname()
+        pi = self.request.getPathinfo()
+        action = u"%s%s" % (sn, pi)
+        _form = html.FORM(action=action)
+        _form.append(html.INPUT(type="hidden", name="action", value="userprefs"))
+        _form.append(html.INPUT(type="hidden", name="handler", value="oid"))
+        return _form
+
+    def _make_row(self, label, cell, **kw):
+        """ Create a row in the form table.
+        """
+        self._table.append(html.TR().extend([
+            html.TD(**kw).extend([html.B().append(label), '   ']),
+            html.TD().extend(cell),
+        ]))
+
+    def _oidlist(self):
+        _ = self.request.getText
+        form = self._make_form()
+        for oid in self.request.user.openids:
+            name = "rm-%s" % sha.new(oid).hexdigest()
+            form.append(html.INPUT(type="checkbox", name=name, id=name))
+            form.append(html.LABEL(for_=name).append(html.Text(oid)))
+            form.append(html.BR())
+        self._make_row(_("Current OpenIDs"), [form], valign='top')
+        label = _("Remove selected")
+        form.append(html.BR())
+        form.append(html.INPUT(type="submit", name="remove", value=label))
+
+    def _addoidform(self):
+        _ = self.request.getText
+        form = self._make_form()
+        # go back to this page
+        form.append(html.INPUT(type="hidden", name="sub", value="oid"))
+        label = _("Add OpenID")
+        form.append(html.INPUT(type="text", size="32",
+                               name="openid_identifier",
+                               id="openididentifier"))
+        form.append(html.BR())
+        form.append(html.INPUT(type="submit", name="add", value=label))
+        self._make_row(_('Add OpenID'), [form])
+
+    def create_form(self):
+        """ Create the complete HTML form code. """
+        _ = self._
+
+        ret = html.P()
+        # Use the user interface language and direction
+        lang_attr = self.request.theme.ui_lang_attr()
+        ret.append(html.Raw('<div %s>' % lang_attr))
+        self._table = html.TABLE(border="0")
+        ret.append(self._table)
+        ret.append(html.Raw("</div>"))
+
+        request = self.request
+
+        if 'openid.prefs.form_html' in request.session:
+            txt = _('OpenID verification requires that you click this button:')
+            # create JS to automatically submit the form if possible
+            submitjs = """<script type="text/javascript">
+<!--//
+document.getElementById("openid_message").submit();
+//-->
+</script>
+"""
+            oidhtml = request.session['openid.prefs.form_html']
+            del request.session['openid.prefs.form_html']
+            return ''.join([txt, oidhtml, submitjs])
+
+        if hasattr(request.user, 'openids') and request.user.openids:
+            self._oidlist()
+        self._addoidform()
+
+        form = self._make_form()
+        label = _("Cancel")
+        form.append(html.INPUT(type="submit", name='cancel', value=label))
+        self._make_row('', [form])
+        return unicode(ret)
--- a/MoinMoin/userprefs/prefs.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/userprefs/prefs.py	Sun Jul 15 23:41:39 2007 +0200
@@ -16,15 +16,9 @@
 #################################################################
 # This is a mess.
 #
-# This plugin is also used by the 'recoverpass' and 'newaccount'
-# actions, and really shouldn't be.
-#
 # The plan for refactoring would be:
-#  1. make the mentioned actions create their own forms and not
-#     use the code here
-#  2. split the plugin into multiple preferences pages:
+# split the plugin into multiple preferences pages:
 #    - account details (name, email, timezone, ...)
-#    - change password
 #    - wiki settings (editor, fancy diffs, theme, ...)
 #    - notification settings (trivial, subscribed pages, ...)
 #    - quick links (or leave in wiki settings?)
@@ -41,6 +35,7 @@
         self._ = request.getText
         self.cfg = request.cfg
         self.title = self._("Preferences")
+        self.name = 'prefs'
 
     def _decode_pagelist(self, key):
         """ Decode list of pages from form input
@@ -90,23 +85,6 @@
             if not theuser.name:
                 return _("Empty user name. Please enter a user name.")
 
-        if not 'password' in theuser.auth_attribs:
-            # try to get the password and pw repeat
-            password = form.get('password', [''])[0]
-            password2 = form.get('password2', [''])[0]
-
-            # Check if password is given and matches with password repeat
-            if password != password2:
-                return _("Passwords don't match!")
-
-            # Encode password
-            if password and not password.startswith('{SHA}'):
-                try:
-                    theuser.enc_password = user.encodePassword(password)
-                except UnicodeError, err:
-                    # Should never happen
-                    return "Can't encode password: %s" % str(err)
-
         if not 'email' in theuser.auth_attribs:
             # try to get the email
             email = wikiutil.clean_input(form.get('email', [theuser.email])[0])
@@ -189,7 +167,7 @@
         # handler for each form field, instead of stuffing them all in
         # one long and inextensible method.  That would allow for
         # plugins to provide methods to validate their fields as well.
-        already_handled = ['name', 'password', 'password2', 'email',
+        already_handled = ['name', 'email',
                            'aliasname', 'edit_rows', 'editor_default',
                            'editor_ui', 'tz_offset', 'datetime_fmt',
                            'theme_name', 'language', 'jid']
@@ -225,6 +203,10 @@
 
         # save data
         theuser.save()
+        if theuser.disabled:
+            # set valid to false so the current request won't
+            # show the user as logged-in any more
+            theuser.valid = False
         self.request.user = theuser
 
         result = _("User preferences saved!")
@@ -350,47 +332,18 @@
                   ]
         return util.web.makeSelection('editor_ui', options, editor_ui)
 
-    def _make_form(self):
-        """ Create the FORM, and the TABLE with the input fields
-        """
-        sn = self.request.getScriptname()
-        pi = self.request.getPathinfo()
-        action = u"%s%s" % (sn, pi)
-        self._form = html.FORM(action=action)
-        self._table = html.TABLE(border="0")
-
-        # Use the user interface language and direction
-        lang_attr = self.request.theme.ui_lang_attr()
-        self._form.append(html.Raw('<div class="userpref"%s>' % lang_attr))
 
-        self._form.append(self._table)
-        self._form.append(html.Raw("</div>"))
-
-
-    def make_row(self, label, cell, **kw):
-        """ Create a row in the form table.
-        """
-        self._table.append(html.TR().extend([
-            html.TD(**kw).extend([html.B().append(label), '   ']),
-            html.TD().extend(cell),
-        ]))
-
-
-    def create_form(self, create_only=False, recover_only=False):
+    def create_form(self):
         """ Create the complete HTML form code. """
         _ = self._
-        self._make_form()
+        self._form = self.make_form()
 
-        if self.request.user.valid and not create_only and not recover_only:
+        if self.request.user.valid:
             buttons = [('save', _('Save')), ('cancel', _('Cancel')), ]
             uf_remove = self.cfg.user_form_remove
             uf_disable = self.cfg.user_form_disable
             for attr in self.request.user.auth_attribs:
-                if attr == 'password':
-                    uf_remove.append(attr)
-                    uf_remove.append('password2')
-                else:
-                    uf_disable.append(attr)
+                uf_disable.append(attr)
             for key, label, type, length, textafter in self.cfg.user_form_fields:
                 default = self.cfg.user_form_defaults[key]
                 if not key in uf_remove:
@@ -476,40 +429,6 @@
                 )
             self._form.append(html.INPUT(type="hidden", name="action", value="userprefs"))
             self._form.append(html.INPUT(type="hidden", name="handler", value="prefs"))
-        elif not recover_only:
-            # Login / register interface
-            buttons = [
-                # IMPORTANT: login should be first to be the default
-                # button when a user hits ENTER.
-                #('login', _('Login')),  # we now have a Login macro
-                ('create', _('Create Profile')),
-                ('cancel', _('Cancel')),
-            ]
-            for key, label, type, length, textafter in self.cfg.user_form_fields:
-                if key in ('name', 'password', 'password2', 'email'):
-                    self.make_row(_(label),
-                              [html.INPUT(type=type, size=length, name=key,
-                                          value=''),
-                               ' ', _(textafter), ])
-            self._form.append(html.INPUT(type="hidden", name="action", value="newaccount"))
-        else:
-            for key, label, type, length, textafter in self.cfg.user_form_fields:
-                if key == 'email':
-                    self.make_row(_(label),
-                              [html.INPUT(type=type, size=length, name=key,
-                                          value=''),
-                               ' ', _(textafter), ])
-            buttons = []
-            self._form.append(html.INPUT(type="hidden", name="action", value="recoverpass"))
-
-        if recover_only and self.cfg.mail_enabled:
-            buttons.append(("account_sendmail", _('Mail me my account data')))
-
-        if create_only:
-            buttons = [("create_only", _('Create Profile'))]
-            if self.cfg.mail_enabled:
-                buttons.append(("create_and_mail", "%s + %s" %
-                                (_('Create Profile'), _('Email'))))
 
         # Add buttons
         button_cell = []
--- a/MoinMoin/userprefs/suid.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/userprefs/suid.py	Sun Jul 15 23:41:39 2007 +0200
@@ -20,13 +20,13 @@
         self._ = request.getText
         self.cfg = request.cfg
         self.title = self._("Switch user")
+        self.name = 'suid'
 
-    def _is_super_user(self):
-        return (self.request.user.isSuperUser() or
-                (not self.request._setuid_real_user is None
-                 and (self.request._setuid_real_user.isSuperUser())))
-
-    allowed = _is_super_user
+    def allowed(self):
+        return (UserPrefBase.allowed(self) and
+                self.request.user.isSuperUser() or
+                (not self.request._setuid_real_user is None and
+                 (self.request._setuid_real_user.isSuperUser())))
 
     def handle_form(self):
         _ = self._
@@ -36,7 +36,7 @@
             return
 
         if (wikiutil.checkTicket(self.request, self.request.form['ticket'][0]) and
-            self.request.request_method == 'POST' and self._is_super_user()):
+            self.request.request_method == 'POST'):
             su_user = form.get('selected_user', [''])[0]
             uid = user.getUserId(self.request, su_user)
             if not uid:
@@ -48,14 +48,16 @@
                 self.request._setuid_real_user = None
             else:
                 theuser = user.User(self.request, uid, auth_method='setuid')
-                theuser.disabled = None
+                # set valid to True so superusers can even switch
+                # to disable accounts
+                theuser.valid = True
                 self.request.session['setuid'] = uid
                 self.request._setuid_real_user = self.request.user
                 # now continue as the other user
                 self.request.user = theuser
-            return  _("Use UserPreferences to change settings of the selected user account, log out to get back to your account.")
+            return  _("You can now change the settings of the selected user account; log out to get back to your account.")
         else:
-            return _("Use UserPreferences to change your settings or create an account.")
+            return None
 
 
     def _user_select(self):
@@ -76,54 +78,18 @@
         current_user = self.request.user.name
         return util.web.makeSelection('selected_user', options, current_user, size=size)
 
-    def _make_form(self):
-        """ Create the FORM, and the TABLE with the input fields
-        """
-        sn = self.request.getScriptname()
-        pi = self.request.getPathinfo()
-        action = u"%s%s" % (sn, pi)
-        self._form = html.FORM(action=action)
-        self._table = html.TABLE(border="0")
-
-        # Use the user interface language and direction
-        lang_attr = self.request.theme.ui_lang_attr()
-        self._form.append(html.Raw('<div class="userpref"%s>' % lang_attr))
-
-        self._form.append(html.INPUT(type="hidden", name="action", value="userprefs"))
-        self._form.append(html.INPUT(type="hidden", name="handler", value="suid"))
-        self._form.append(self._table)
-        self._form.append(html.Raw("</div>"))
-
-
-    def _make_row(self, label, cell, **kw):
-        """ Create a row in the form table.
-        """
-        self._table.append(html.TR().extend([
-            html.TD(**kw).extend([html.B().append(label), '   ']),
-            html.TD().extend(cell),
-        ]))
-
-
     def create_form(self):
         """ Create the complete HTML form code. """
         _ = self._
-        self._make_form()
+        form = self.make_form(html.Text(_('As a superuser, you can temporarily '
+                                          'assume the identity of another user.')))
 
-        if (self.request.user.isSuperUser() or
-            (not self.request._setuid_real_user is None and
-             self.request._setuid_real_user.isSuperUser())):
-            ticket = wikiutil.createTicket(self.request)
-            self._make_row(_('Select User'), [self._user_select()])
-            self._form.append(html.INPUT(type="hidden", name="ticket", value="%s" % ticket))
-            buttons = [("select_user", _('Select User')),
-                       ("cancel", _('Cancel'))]
-            button_cell = []
-            for name, label in buttons:
-                button_cell.extend([
-                    html.INPUT(type="submit", name=name, value=label),
-                    ' ',
-                ])
-            self._make_row('', button_cell)
-            return unicode(self._form)
-
-        return u''
+        ticket = wikiutil.createTicket(self.request)
+        self.make_row('Select User', [self._user_select()], valign="top")
+        form.append(html.INPUT(type="hidden", name="ticket", value="%s" % ticket))
+        self.make_row('', [html.INPUT(type="submit", name="select_user",
+                                      value=_('Select User')),
+                           ' ',
+                           html.INPUT(type="submit", name="cancel",
+                                      value=_('Cancel'))])
+        return unicode(form)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/moinoid.py	Sun Jul 15 23:41:39 2007 +0200
@@ -0,0 +1,112 @@
+"""
+    MoinMoin - OpenID utils
+
+    @copyright: 2006, 2007 Johannes Berg <johannes@sipsolutions.net>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import caching
+from openid import oidutil
+from openid.store.interface import OpenIDStore
+from openid.association import Association
+from openid.store import nonce
+import logging
+from sha import sha
+from random import randint
+import time
+
+# redirect openid logging to moin log
+def log(msg, level=0):
+    logging.log(level, msg)
+
+oidutil.log = log
+
+def strbase64(value):
+    from base64 import encodestring
+    return encodestring(str(value)).replace('\n', '')
+
+
+def _cleanup_nonces(request):
+    cachelist = caching.get_cache_list(request, 'openid-nonce', 'farm')
+    # really openid should have a method to check this...
+    texpired = time.time() - nonce.SKEW
+    for name in cachelist:
+        entry = caching.CacheEntry(request, 'openid-nonce', name,
+                                   scope='farm', use_pickle=False)
+        try:
+            timestamp = int(entry.content())
+            if timestamp < texpired:
+                entry.remove()
+        except caching.CacheError:
+            pass
+
+
+class MoinOpenIDStore(OpenIDStore):
+    '''OpenIDStore for MoinMoin'''
+    def __init__(self, request):
+        self.request = request
+        OpenIDStore.__init__(self)
+
+    def key(self, url):
+        '''return cache key'''
+        return sha(url).hexdigest()
+
+    def storeAssociation(self, server_url, association):
+        ce = caching.CacheEntry(self.request, 'openid', self.key(server_url),
+                                scope='wiki', use_pickle=True)
+        if ce.exists():
+            assocs = ce.content()
+        else:
+            assocs = []
+        assocs += [association.serialize()]
+        ce.update(assocs)
+
+    def getAssociation(self, server_url, handle=None):
+        ce = caching.CacheEntry(self.request, 'openid', self.key(server_url),
+                                scope='wiki', use_pickle=True)
+        if not ce.exists():
+            return None
+        assocs = ce.content()
+        found = False
+        for idx in xrange(len(assocs)-1, -1, -1):
+            assoc_str = assocs[idx]
+            association = Association.deserialize(assoc_str)
+            if association.getExpiresIn() == 0:
+                del assocs[idx]
+            else:
+                if handle is None or association.handle == handle:
+                    found = True
+                    break
+        ce.update(assocs)
+        if found:
+            return association
+        return None
+
+    def removeAssociation(self, server_url, handle):
+        ce = caching.CacheEntry(self.request, 'openid', self.key(server_url),
+                                scope='wiki', use_pickle=True)
+        if not ce.exists():
+            return
+        assocs = ce.content()
+        for idx in xrange(len(assocs)-1, -1, -1):
+            assoc_str = assocs[idx]
+            association = Association.deserialize(assoc_str)
+            if association.handle == handle:
+                del assocs[idx]
+        if len(assocs):
+            ce.update(assocs)
+        else:
+            ce.remove()
+
+    def useNonce(self, server_url, timestamp, salt):
+        val = ''.join([str(server_url), str(timestamp), str(salt)])
+        csum = sha(val).hexdigest()
+        ce = caching.CacheEntry(self.request, 'openid-nonce', csum,
+                                scope='farm', use_pickle=False)
+        if ce.exists():
+            # nonce already used!
+            return False
+        ce.update(str(timestamp))
+        if randint(0, 999) == 0:
+            self.request.add_finisher(_cleanup_nonces)
+        return True
--- a/MoinMoin/widget/html.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/widget/html.py	Sun Jul 15 23:41:39 2007 +0200
@@ -431,8 +431,26 @@
     "form field label text"
     _ATTRS = {
         'class': None,
+        'for_': None,
     }
 
+    def _openingtag(self):
+        result = [self.tagname()]
+        attrs = self.attrs.items()
+        if _SORT_ATTRS:
+            attrs.sort()
+        for key, val in attrs:
+            key = key.lower()
+            if key == 'for_':
+                key = 'for'
+            if key in self._BOOL_ATTRS:
+                if val:
+                    result.append(key)
+            else:
+                result.append(u'%s="%s"' % (key, wikiutil.escape(val, 1)))
+        return ' '.join(result)
+
+
 class LI(CompositeElement):
     "list item"
     _ATTRS = {
--- a/MoinMoin/wikiutil.py	Sun Jul 15 23:41:14 2007 +0200
+++ b/MoinMoin/wikiutil.py	Sun Jul 15 23:41:39 2007 +0200
@@ -482,7 +482,7 @@
 
 # Quoting of wiki names, file names, etc. (in the wiki markup) -----------------------------------
 
-QUOTE_CHARS = u"'\""
+QUOTE_CHARS = u'"'
 
 def quoteName(name):
     """ put quotes around a given name """
--- a/wiki/htdocs/classic/css/screen.css	Sun Jul 15 23:41:14 2007 +0200
+++ b/wiki/htdocs/classic/css/screen.css	Sun Jul 15 23:41:39 2007 +0200
@@ -395,3 +395,8 @@
 	font-weight: bold;
 }
 
+#openididentifier {
+    background: url(../../common/openid.png) no-repeat;
+    background-position: 0 50%;
+    padding-left: 18px;
+}
Binary file wiki/htdocs/common/openid.png has changed
--- a/wiki/htdocs/modern/css/screen.css	Sun Jul 15 23:41:14 2007 +0200
+++ b/wiki/htdocs/modern/css/screen.css	Sun Jul 15 23:41:39 2007 +0200
@@ -457,4 +457,8 @@
     margin: 2px;
 }
         
-
+#openididentifier {
+    background: url(../../common/openid.png) no-repeat;
+    background-position: 0 50%;
+    padding-left: 18px;
+}
--- a/wiki/htdocs/rightsidebar/css/screen.css	Sun Jul 15 23:41:14 2007 +0200
+++ b/wiki/htdocs/rightsidebar/css/screen.css	Sun Jul 15 23:41:39 2007 +0200
@@ -347,3 +347,8 @@
 	font-weight: bold;
 }
 
+#openididentifier {
+    background: url(../../common/openid.png) no-repeat;
+    background-position: 0 50%;
+    padding-left: 18px;
+}