changeset 2357:39691c426192

Merge main.
author Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl>
date Wed, 11 Jul 2007 02:51:26 +0200
parents 91b9d9e40e07 (current diff) 1f449e482bcc (diff)
children 148862d36d4a
files MoinMoin/user.py MoinMoin/userprefs/__init__.py
diffstat 15 files changed, 718 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/action/userprefs.py	Tue Jul 10 20:57:07 2007 +0200
+++ b/MoinMoin/action/userprefs.py	Wed Jul 11 02:51:26 2007 +0200
@@ -55,16 +55,18 @@
     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
+        return _create_prefs_page(request), None, msg
 
     obj = cls(request)
-    return obj.create_form(), obj.title, None
+    return obj.create_form(), obj.title, msg
 
 
 def execute(pagename, request):
--- a/MoinMoin/auth/__init__.py	Tue Jul 10 20:57:07 2007 +0200
+++ b/MoinMoin/auth/__init__.py	Wed Jul 11 02:51:26 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	Wed Jul 11 02:51:26 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	Wed Jul 11 02:51:26 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/request/__init__.py	Tue Jul 10 20:57:07 2007 +0200
+++ b/MoinMoin/request/__init__.py	Wed Jul 11 02:51:26 2007 +0200
@@ -603,16 +603,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 +625,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	Tue Jul 10 20:57:07 2007 +0200
+++ b/MoinMoin/user.py	Wed Jul 11 02:51:26 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	Tue Jul 10 20:57:07 2007 +0200
+++ b/MoinMoin/userform/login.py	Wed Jul 11 02:51:26 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	Tue Jul 10 20:57:07 2007 +0200
+++ b/MoinMoin/userprefs/__init__.py	Wed Jul 11 02:51:26 2007 +0200
@@ -39,6 +39,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
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/userprefs/oid.py	Wed Jul 11 02:51:26 2007 +0200
@@ -0,0 +1,208 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - OpenID preferences
+
+    @copyright: 2007     MoinMoin:JohannesBerg
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import wikiutil
+from MoinMoin.widget import html
+from MoinMoin.userprefs import UserPrefBase
+from MoinMoin.auth.openidrp import OpenIDAuth
+import sha
+from MoinMoin.util.moinoid import MoinOpenIDStore
+from openid.consumer import consumer
+from openid.yadis.discover import DiscoveryFailure
+from openid.fetchers import HTTPFetchingError
+
+
+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):
+        for authm in self.request.cfg.auth:
+            if isinstance(authm, OpenIDAuth):
+                return True
+        return False
+
+    def _handle_remove(self):
+        _ = self.request.getText
+        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 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 = ''.join([request.getBaseURL(), '?', 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 = ''.join([request.getBaseURL(), '?', 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 info.identity_url in request.user.openids:
+                request.user.openids.append(info.identity_url)
+                request.user.save()
+                return _("OpenID added successfully.")
+            else:
+                return _("OpenID is already present.")
+        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 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/moinoid.py	Wed Jul 11 02:51:26 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	Tue Jul 10 20:57:07 2007 +0200
+++ b/MoinMoin/widget/html.py	Wed Jul 11 02:51:26 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/wiki/htdocs/classic/css/screen.css	Tue Jul 10 20:57:07 2007 +0200
+++ b/wiki/htdocs/classic/css/screen.css	Wed Jul 11 02:51:26 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	Tue Jul 10 20:57:07 2007 +0200
+++ b/wiki/htdocs/modern/css/screen.css	Wed Jul 11 02:51:26 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	Tue Jul 10 20:57:07 2007 +0200
+++ b/wiki/htdocs/rightsidebar/css/screen.css	Wed Jul 11 02:51:26 2007 +0200
@@ -347,3 +347,8 @@
 	font-weight: bold;
 }
 
+#openididentifier {
+    background: url(../../common/openid.png) no-repeat;
+    background-position: 0 50%;
+    padding-left: 18px;
+}