changeset 4891:42b73b7ae79a

merged moin/1.9-openid repo
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Wed, 22 Jul 2009 21:50:03 +0200
parents 2752a5368008 (current diff) 55dc44a8e9ee (diff)
children 802cc30f1937
files
diffstat 13 files changed, 407 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/action/login.py	Wed Jul 22 13:25:17 2009 +0200
+++ b/MoinMoin/action/login.py	Wed Jul 22 21:50:03 2009 +0200
@@ -33,7 +33,7 @@
         """
         _ = self._
         request = self.request
-        form = html.FORM(method='POST', name='logincontinue')
+        form = html.FORM(method='POST', name='logincontinue', action=self.pagename)
         form.append(html.INPUT(type='hidden', name='login', value='login'))
         form.append(html.INPUT(type='hidden', name='stage',
                                value=request._login_multistage_name))
@@ -54,7 +54,7 @@
     def handle(self):
         _ = self._
         request = self.request
-        form = request.form
+        form = request.values
 
         error = None
 
--- a/MoinMoin/auth/__init__.py	Wed Jul 22 13:25:17 2009 +0200
+++ b/MoinMoin/auth/__init__.py	Wed Jul 22 21:50:03 2009 +0200
@@ -158,8 +158,10 @@
               'stage': auth_name}
     fields.update(extra_fields)
     if request.page:
+        logging.debug("request.page.url: " + request.page.url(request, querystr=fields))
         return request.page.url(request, querystr=fields)
     else:
+        logging.debug("request.abs_href: " + request.abs_href(**fields))
         return request.abs_href(**fields)
 
 class LoginReturn(object):
@@ -248,12 +250,24 @@
 
     def login_hint(self, request):
         _ = request.getText
+        #if request.cfg.openidrp_registration_url:
+        #    userprefslink = request.cfg.openidrp_registration_url
+        #else:
         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>') % {
-               'userprefslink': userprefslink,
+
+        msg = ''
+        #if request.cfg.openidrp_allow_registration:
+        msg = _('If you do not have an account, <a href="%(userprefslink)s">you can create one now</a>. ') % {
+              'userprefslink': userprefslink}
+        msg += _('<a href="%(sendmypasswordlink)s">Forgot your password?</a>') % {
                'sendmypasswordlink': sendmypasswordlink}
+        return msg
+
+        #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>') % {
+        #       'userprefslink': userprefslink,
+        #       'sendmypasswordlink': sendmypasswordlink}
 
 
 class GivenAuth(BaseAuth):
--- a/MoinMoin/auth/openidrp.py	Wed Jul 22 13:25:17 2009 +0200
+++ b/MoinMoin/auth/openidrp.py	Wed Jul 22 21:50:03 2009 +0200
@@ -5,6 +5,9 @@
     @copyright: 2007 MoinMoin:JohannesBerg
     @license: GNU GPL, see COPYING for details.
 """
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
 from MoinMoin.util.moinoid import MoinOpenIDStore
 from MoinMoin import user
 from MoinMoin.auth import BaseAuth
@@ -15,12 +18,13 @@
 from MoinMoin.auth import CancelLogin, ContinueLogin
 from MoinMoin.auth import MultistageFormLogin, MultistageRedirectLogin
 from MoinMoin.auth import get_multistage_continuation_url
-
+from werkzeug.utils import url_encode
 
 class OpenIDAuth(BaseAuth):
     login_inputs = ['openid_identifier']
     name = 'openid'
     logout_possible = True
+    auth_attribs = ()
 
     def __init__(self, modify_request=None,
                        update_user=None,
@@ -28,9 +32,9 @@
                        forced_service=None,
                        idselector_com=None):
         BaseAuth.__init__(self)
-        self._modify_request = modify_request or (lambda x: None)
-        self._update_user = update_user or (lambda i, u: None)
-        self._create_user = create_user or (lambda i, u: None)
+        self._modify_request = modify_request or (lambda x, c: None)
+        self._update_user = update_user or (lambda i, u, c: None)
+        self._create_user = create_user or (lambda i, u, c: None)
         self._forced_service = forced_service
         self._idselector_com = idselector_com
         if forced_service:
@@ -41,13 +45,14 @@
         if create:
             # pass in a created but unsaved user object
             u = user.User(request, auth_method=self.name,
-                          auth_username=request.session['openid.id'])
+                          auth_username=request.session['openid.id'],
+                          auth_attribs=self.auth_attribs)
             # invalid name
             u.name = ''
-            u = self._create_user(request.session['openid.info'], u)
+            u = self._create_user(request.session['openid.info'], u, request.cfg)
 
         if u:
-            self._update_user(request.session['openid.info'], u)
+            self._update_user(request.session['openid.info'], u, request.cfg)
 
             # just in case the wiki admin screwed up
             if (not user.isValidName(request, u.name) or
@@ -71,6 +76,7 @@
         # that they want to use on this wiki
         # XXX: request nickname from OP and suggest using it
         # (if it isn't in use yet)
+        logging.debug("running _get_account_name")
         _ = request.getText
         form.append(html.INPUT(type='hidden', name='oidstage', value='2'))
         table = html.TABLE(border='0')
@@ -135,16 +141,19 @@
         oidconsumer = consumer.Consumer(request.session,
                                         MoinOpenIDStore(request))
         query = {}
-        for key in request.form:
-            query[key] = request.form[key]
+        for key in request.values.keys():
+            query[key] = request.values.get(key)
         current_url = get_multistage_continuation_url(request, self.name,
                                                       {'oidstage': '1'})
         info = oidconsumer.complete(query, current_url)
         if info.status == consumer.FAILURE:
+            logging.debug(_("OpenID error: %s.") % info.message)
             return CancelLogin(_('OpenID error: %s.') % info.message)
         elif info.status == consumer.CANCEL:
+            logging.debug(_("OpenID verification canceled."))
             return CancelLogin(_('Verification canceled.'))
         elif info.status == consumer.SUCCESS:
+            logging.debug(_("OpenID success. id: %s") % info.identity_url)
             request.session['openid.id'] = info.identity_url
             request.session['openid.info'] = info
 
@@ -152,7 +161,8 @@
             uid = user.getUserIdByOpenId(request, info.identity_url)
             if uid:
                 u = user.User(request, id=uid, auth_method=self.name,
-                              auth_username=info.identity_url)
+                              auth_username=info.identity_url,
+                              auth_attribs=self.auth_attribs)
             else:
                 u = None
 
@@ -163,14 +173,16 @@
 
             # if no user found, then we need to ask for a username,
             # possibly associating an existing account.
-            request.session['openid.id'] = info.identity_url
+            logging.debug("OpenID: No user found, prompting for username")
+            #request.session['openid.id'] = info.identity_url
             return MultistageFormLogin(self._get_account_name)
         else:
+            logging.debug(_("OpenID failure"))
             return CancelLogin(_('OpenID failure.'))
 
     def _handle_name_continuation(self, request):
         if not 'openid.id' in request.session:
-            return CancelLogin(None)
+            return CancelLogin(_('No OpenID found in session.'))
 
         _ = request.getText
         newname = request.form.get('username', '')
@@ -184,7 +196,8 @@
         if not uid:
             # we can create a new user with this name :)
             u = user.User(request, auth_method=self.name,
-                          auth_username=request.session['openid.id'])
+                          auth_username=request.session['openid.id'],
+                          auth_attribs=self.auth_attribs)
             u.name = newname
             u = self._handle_user_data(request, u)
             return ContinueLogin(u)
@@ -195,7 +208,7 @@
 
     def _handle_associate_continuation(self, request):
         if not 'openid.id' in request.session:
-            return CancelLogin()
+            return CancelLogin(_('No OpenID found in session.'))
 
         _ = request.getText
         username = request.form.get('username', '')
@@ -204,7 +217,8 @@
             return self._handle_name_continuation(request)
         u = user.User(request, name=username, password=password,
                       auth_method=self.name,
-                      auth_username=request.session['openid.id'])
+                      auth_username=request.session['openid.id'],
+                      auth_attribs=self.auth_attribs)
         if u.valid:
             self._handle_user_data(request, u)
             return ContinueLogin(u, _('Your account is now associated to your OpenID.'))
@@ -214,14 +228,19 @@
             return MultistageFormLogin(assoc)
 
     def _handle_continuation(self, request):
-        oidstage = request.form.get('oidstage', '0')
+        _ = request.getText
+        oidstage = request.values.get('oidstage')
         if oidstage == '1':
+            logging.debug(_('OpenID: handle verify continuation'))
             return self._handle_verify_continuation(request)
         elif oidstage == '2':
+            logging.debug(_('OpenID: handle name continuation'))
             return self._handle_name_continuation(request)
         elif oidstage == '3':
+            logging.debug(_('OpenID: handle associate continuation'))
             return self._handle_associate_continuation(request)
-        return CancelLogin()
+        logging.debug(_('OpenID: unknown continuation stage'))
+        return CancelLogin(_('OpenID error: unknown continuation stage'))
 
     def _openid_form(self, request, form, oidhtml):
         _ = request.getText
@@ -278,7 +297,7 @@
             if oidreq is None:
                 return ContinueLogin(None, _('No OpenID.'))
 
-            self._modify_request(oidreq)
+            self._modify_request(oidreq, request.cfg)
 
             return_to = get_multistage_continuation_url(request, self.name,
                                                         {'oidstage': '1'})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/openidrp_ext/__init__.py	Wed Jul 22 21:50:03 2009 +0200
@@ -0,0 +1,8 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin OpenID Relying Party Extensions
+
+@copyright: 2007-20099 Canonical, Inc.
+@license: GNU GPL, see COPYING for details.
+"""
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/openidrp_ext/openidrp_sreg.py	Wed Jul 22 21:50:03 2009 +0200
@@ -0,0 +1,90 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Simple Registration Extension for OpenID authorization
+
+    @copyright: 2009 Canonical, Inc.
+    @license: GNU GPL, see COPYING for details.
+"""
+#from MoinMoin.util.moinoid import MoinOpenIDStore
+from MoinMoin import user
+from MoinMoin.auth import BaseAuth
+from MoinMoin.auth.openidrp import OpenIDAuth
+#from openid.consumer import consumer
+#from openid.yadis.discover import DiscoveryFailure
+#from openid.fetchers import HTTPFetchingError
+#from MoinMoin.widget import html
+#from MoinMoin.auth import CancelLogin, ContinueLogin
+#from MoinMoin.auth import MultistageFormLogin, MultistageRedirectLogin
+#from MoinMoin.auth import get_multistage_continuation_url
+
+from openid.extensions.sreg import *
+from MoinMoin import i18n
+from datetime import datetime, timedelta
+from pytz import timezone
+import pytz
+
+OpenIDAuth.auth_attribs = ('name', 'email', 'aliasname', 'language', 'tz_offset')
+
+openidrp_sreg_required = ['nickname', 'email', 'timezone']
+openidrp_sreg_optional = ['fullname', 'language']
+openidrp_sreg_username_field = 'nickname'
+
+def openidrp_sreg_modify_request(oidreq, cfg):
+    oidreq.addExtension(SRegRequest(required=cfg.openidrp_sreg_required,
+                                    optional=cfg.openidrp_sreg_optional))
+
+def openidrp_sreg_create_user(info, u, cfg):
+    sreg = _openidrp_sreg_extract_values(info)
+    if sreg and sreg[cfg.openidrp_sreg_username_field] != '':
+        u.name = sreg[cfg.openidrp_sreg_username_field]
+    return u
+
+def openidrp_sreg_update_user(info, u, cfg):
+    sreg = _openidrp_sreg_extract_values(info)
+    if sreg:
+        u.name = sreg[cfg.openidrp_sreg_username_field]
+        if sreg['email'] != '':
+            u.email = sreg['email']
+        if sreg['language'] != '':
+            u.language = sreg['language']
+        if sreg['timezone'] != '':
+            u.tz_offset = sreg['timezone']
+        if sreg['fullname'] != '':
+            u.fullname = sreg['fullname']
+
+def _openidrp_sreg_extract_values(info):
+    # Pull SREG data here instead of asking user
+    sreg_resp = SRegResponse.fromSuccessResponse(info)
+    sreg = {'nickname': '', 'email': '', 'fullname': '',
+            'dob': '0000-00-00', 'gender': '', 'postcode': '',
+            'country': '', 'language': '', 'timezone' : ''}
+    if sreg_resp:
+        if sreg_resp.get('nickname'):
+            sreg['nickname'] = sreg_resp.get('nickname')
+        if sreg_resp.get('fullname'):
+            sreg['fullname'] = sreg_resp.get('fullname')
+        if sreg_resp.get('email'):
+            sreg['email'] = sreg_resp.get('email')
+        # Language must be a valid value
+        # check the MoinMoin list, or restrict to first 2 chars
+        if sreg_resp.get('language'):
+            # convert unknown codes to 2 char format
+            langs = i18n.wikiLanguages().items()
+            sreg['language'] = sreg_resp.get('language')
+            lang_found = False
+            for lang in langs:
+                if lang[0] == sreg['language']:
+                    lang_found = True
+            if not lang_found:
+                if langs[sreg['language'][0:2]]:
+                    sreg['language'] = sreg['language'][0:2]
+        # Timezone must be converted to offset in seconds
+        if sreg_resp.get('timezone'):
+            user_tz = timezone(sreg_resp.get('timezone').encode('ascii'))
+            if user_tz:
+                user_utcoffset = user_tz.utcoffset(datetime.utcnow())
+                sreg['timezone'] = user_utcoffset.days * 24 * 60 * 60 + user_utcoffset.seconds
+            else:
+                sreg['timezone'] = 0
+    return sreg
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/openidrp_ext/openidrp_teams.py	Wed Jul 22 21:50:03 2009 +0200
@@ -0,0 +1,116 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Launchpad Teams Extension for OpenID authorization
+
+    @copyright: 2009 Canonical, Inc.
+    @license: GNU GPL, see COPYING for details.
+"""
+import re
+import logging
+import copy
+
+#from MoinMoin.util.moinoid import MoinOpenIDStore
+from MoinMoin import user
+from MoinMoin.auth import BaseAuth
+from MoinMoin.auth.openidrp import OpenIDAuth
+#OpenIDSREGAuth
+#from openid.consumer import consumer
+#from openid.yadis.discover import DiscoveryFailure
+#from openid.fetchers import HTTPFetchingError
+#from MoinMoin.widget import html
+#from MoinMoin.auth import CancelLogin, ContinueLogin
+#from MoinMoin.auth import MultistageFormLogin, MultistageRedirectLogin
+#from MoinMoin.auth import get_multistage_continuation_url
+
+from openid.extensions.teams import TeamsRequest, TeamsResponse, supportsTeams
+from MoinMoin import wikiutil
+from MoinMoin.PageEditor import PageEditor, conflict_markers
+from MoinMoin.Page import Page
+
+def openidrp_teams_modify_request(oidreq, cfg):
+    # Request Launchpad teams information, if configured
+    # should also check supportsTeams() result
+    #if teams_extension_avail and len(cfg.openidrp_authorized_teams) > 0:
+    if len(cfg.openidrp_authorized_teams) > 0:
+        oidreq.addExtension(TeamsRequest(cfg.openidrp_authorized_teams))
+
+def openidrp_teams_create_user(info, u, cfg):
+    # Check for Launchpad teams data in response
+    teams = None
+    #if teams_extension_avail and len(cfg.openidrp_authorized_teams) > 0:
+    teams_response = TeamsResponse.fromSuccessResponse(info)
+    teams = teams_response.is_member
+    if teams:
+        _save_teams_acl(u, teams, cfg)
+    return u
+
+def openidrp_teams_update_user(info, u, cfg):
+    teams = None
+    teams_response = TeamsResponse.fromSuccessResponse(info)
+    teams = teams_response.is_member
+    if teams:
+        _save_teams_acl(u, teams, cfg)
+
+# Take a list of Launchpad teams and add the user to the ACL pages
+# ACL group names cannot have "-" in them, although team names do.
+def _save_teams_acl(u, teams, cfg):
+    logging.log(logging.INFO, "running save_teams_acl...")
+
+    # remove any teams the user is no longer in
+    if not hasattr(u, 'teams'):
+        u.teams = []
+    logging.log(logging.INFO, "old teams: " + str(u.teams)
+        + "  new teams: " + str(teams))
+
+    for t in u.teams:
+        if not t in teams:
+            logging.log(logging.INFO, "remove user from team: " + t)
+            team = t.strip().replace("-", "")
+            _remove_user_from_team(u, team, cfg)
+
+    for t in teams:
+        team = t.strip().replace("-", "")
+        if not team:
+            continue
+        logging.log(logging.INFO, "Launchpad team: "  + team)
+        _add_user_to_team(u, team, cfg)
+
+    u.teams = teams
+    u.save()
+
+def _add_user_to_team(u, team, cfg):
+    # use admin account to create or edit ACL page
+    # http://moinmo.in/MoinDev/CommonTasks
+    acl_request = u._request
+    acl_request.user = user.User(acl_request, None, cfg.openidrp_acl_admin)
+    pe = PageEditor(acl_request, team + cfg.openidrp_acl_page_postfix)
+    acl_text = pe.get_raw_body()
+    logging.log(logging.INFO, "ACL Page content: " + acl_text)
+    # make sure acl command is first line of document
+    # only the admin user specified in wikiconfig should
+    # be allowed to change these acl files
+    if not acl_text or acl_text == "" or acl_text[0] != "#":
+        acl_text = "#acl Known:read All:\n" + acl_text
+    # does ACL want uid, name, username, auth_username?
+    p = re.compile(ur"^ \* %s" % u.name, re.MULTILINE)
+    if not p.search(acl_text):
+        logging.log(logging.INFO, "did not find user %s in acl, adding..." % u.name)
+        acl_text += u" * %s\n" % u.name
+        pe.saveText(acl_text, 0)
+
+def _remove_user_from_team(u, team, cfg):
+    acl_request = u._request
+    acl_request.user = user.User(acl_request, None, cfg.openidrp_acl_admin)
+    pe = PageEditor(acl_request, team + cfg.openidrp_acl_page_postfix)
+    acl_text = pe.get_raw_body()
+    logging.log(logging.INFO, "ACL Page content: " + acl_text)
+    # does ACL want uid, name, username, auth_username?
+    p = re.compile(ur"^ \* %s" % u.name, re.MULTILINE)
+    if p.search(acl_text):
+        logging.log(logging.INFO, "found user %s in acl, removing..." % u.name)
+        acl_text = acl_text.replace(" * %s\n" % u.name, "")
+        try:
+            pe.saveText(acl_text, 0)
+        except PageEditor.EmptyPage:
+            pe.deletePage()
+
--- a/MoinMoin/userform/login.py	Wed Jul 22 13:25:17 2009 +0200
+++ b/MoinMoin/userform/login.py	Wed Jul 22 21:50:03 2009 +0200
@@ -39,7 +39,7 @@
             hint = authm.login_hint(request)
             if hint:
                 hints.append(hint)
-        self._form = html.FORM(action=action, name="loginform")
+        self._form = html.FORM(action=action, name="loginform", id="loginform")
         self._table = html.TABLE(border="0")
 
         # Use the user interface language and direction
@@ -67,20 +67,49 @@
                 ),
             ])
 
+        # Restrict type of input available for OpenID input
+        # based on wiki configuration.
         if 'openid_identifier' in cfg.auth_login_inputs:
-            self.make_row(_('OpenID'), [
-                html.INPUT(
-                    type="text", size="32", name="openid_identifier",
-                    id="openididentifier"
-                ),
-            ])
+            if len(cfg.openidrp_allowed_op) == 1:
+                self.make_row(_('OpenID'), [
+                     html.INPUT(
+                         type="hidden", name="openid_identifier",
+                         value=cfg.openidrp_allowed_op[0]
+                     ),
+                ])
+            elif len(cfg.openidrp_allowed_op) > 1:
+                op_select = html.SELECT(name="openid_identifier",
+                    id="openididentifier")
+                for op_uri in cfg.openidrp_allowed_op:
+                    op_select.append(html.OPTION(value=op_uri).append(
+                        html.Raw(op_uri)))
 
+                self.make_row(_('OpenID'), [op_select,])
+            else:
+                self.make_row(_('OpenID'), [
+                    html.INPUT(
+                        type="text", size="32", name="openid_identifier",
+                        id="openididentifier"
+                    ),
+                ])
+
+        # Need both hidden field and submit values for auto-submit to work
         self.make_row('', [
+            html.INPUT(type="hidden", name="login", value=_('Login')),
             html.INPUT(
                 type="submit", name='login', value=_('Login')
             ),
         ])
 
+        # Automatically submit the form if only a single OpenID Provider is allowed
+        if 'openid_identifier' in cfg.auth_login_inputs and len(cfg.openidrp_allowed_op) == 1:
+            self._form.append("""<script type="text/javascript">
+<!--//
+document.getElementById("loginform").submit();
+//-->
+</script>
+""")
+
         return unicode(self._form)
 
 def getLogin(request):
--- a/MoinMoin/userprefs/prefs.py	Wed Jul 22 13:25:17 2009 +0200
+++ b/MoinMoin/userprefs/prefs.py	Wed Jul 22 21:50:03 2009 +0200
@@ -239,7 +239,7 @@
         'rfc': '%a %b %d %H:%M:%S %Y & %a %b %d %Y',
     }
 
-    def _tz_select(self):
+    def _tz_select(self, enabled=True):
         """ Create time zone selection. """
         tz = 0
         if self.request.user.valid:
@@ -261,7 +261,7 @@
                 ),
             ))
 
-        return util.web.makeSelection('tz_offset', options, str(tz))
+        return util.web.makeSelection('tz_offset', options, str(tz), 1, False, enabled)
 
 
     def _dtfmt_select(self):
@@ -279,7 +279,7 @@
         return util.web.makeSelection('datetime_fmt', options, selected)
 
 
-    def _lang_select(self):
+    def _lang_select(self, enabled=True):
         """ Create language selection. """
         from MoinMoin import i18n
         _ = self._
@@ -291,7 +291,7 @@
             name = lang[1]['x-language']
             options.append((lang[0], name))
 
-        return util.web.makeSelection('language', options, cur_lang)
+        return util.web.makeSelection('language', options, cur_lang, 1, False, enabled)
 
     def _theme_select(self):
         """ Create theme selection. """
--- a/MoinMoin/util/web.py	Wed Jul 22 13:25:17 2009 +0200
+++ b/MoinMoin/util/web.py	Wed Jul 22 21:50:03 2009 +0200
@@ -25,7 +25,7 @@
         return result
 
 
-def makeSelection(name, values, selectedval=None, size=1, multiple=False):
+def makeSelection(name, values, selectedval=None, size=1, multiple=False, enabled=True):
     """ Make a HTML <select> element named `name` from a value list.
         The list can either be a list of strings, or a list of
         (value, label) tuples.
@@ -33,7 +33,7 @@
         `selectedval` is the value that should be pre-selected.
     """
     from MoinMoin.widget import html
-    result = html.SELECT(name=name, size="%d" % int(size), multiple=multiple)
+    result = html.SELECT(name=name, size="%d" % int(size), multiple=multiple, disabled=not enabled)
     for val in values:
         if not isinstance(val, type(())):
             val = (val, val)
@@ -44,7 +44,7 @@
 
     return result
 
-def makeMultiSelection(name, values, selectedvals=None, size=5):
+def makeMultiSelection(name, values, selectedvals=None, size=5, enabled=True):
     """Make a HTML multiple <select> element with named `name` from a value list.
 
     The list can either be a list of strings, or a list of (value, label) tuples.
@@ -52,7 +52,7 @@
 
     """
     from MoinMoin.widget import html
-    result = html.SELECT(name=name, size="%d" % int(size), multiple=True)
+    result = html.SELECT(name=name, size="%d" % int(size), multiple=True, disabled=not enabled)
     for val in values:
         if not isinstance(val, type(())):
             val = (val, val)
--- a/MoinMoin/widget/html.py	Wed Jul 22 13:25:17 2009 +0200
+++ b/MoinMoin/widget/html.py	Wed Jul 22 21:50:03 2009 +0200
@@ -308,6 +308,7 @@
         'onreset': None,
         'onsubmit': None,
         'target': None,
+        'id': None,
     }
     _DEFAULT_ATTRS = {
         'method': 'POST',
@@ -551,6 +552,7 @@
         'onfocus': None,
         'size': None,
         'tabindex': None,
+        'id': None,
     }
 
 class SMALL(CompositeElement):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/CHANGES.openid	Wed Jul 22 21:50:03 2009 +0200
@@ -0,0 +1,25 @@
+Version 1.9.?
+
+  New features:
+  * OpenID: Support for Simple Registration (SREG) extension.
+    Basic profile fields can be copied from OpenID provider when logging in.
+  * OpenID: Support for Teams extension.
+    If your OpenID provider supports the Teams extension (i.e. Launchpad),
+    MoinMoin can be configured to generate 
+  * OpenID: Ability to accept logins from specific OpenID providers.
+    Login form changes based on configuration for better usability:
+    * 0 providers: normal text input box for OpenID URL
+    * 1 provider: hidden field, automatic form submission with JavaScript
+    * 2+ providers: select field, uses directed identity
+
+  Fixes:
+  * OpenID request processing now works with new WSGI refactoring.
+  * Always return error messages with CancelLogin if OpenID process fails.
+
+  Other changes:
+  * Added disabled state for HTML select fields.
+  * Allowed disabling of timezone and language user prefs if they are
+    part of the user's login fields (i.e. OpenID SREG).
+  * Added option to disable local registration links and direct user
+    to registration page at an OpenID provider instead.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/config/more_samples/openid_sreg_wikiconfig_snippet	Wed Jul 22 21:50:03 2009 +0200
@@ -0,0 +1,37 @@
+    # This is a sample configuration snippet that shows moin's openid sreg configuration
+    # See HelpOnOpenID, HelpOnAuthentication and HelpOnConfiguration for more info.
+
+    from MoinMoin.auth.openidrp import OpenIDAuth
+    from MoinMoin.auth.openidrp_ext.openidrp_sreg import *
+
+    auth = [
+        OpenIDAuth(modify_request=openidrp_sreg_modify_request,
+                   update_user=openidrp_sreg_update_user,
+                   create_user=openidrp_sreg_create_user),
+        # other auth methods can go here
+        #MoinAuth()
+    ]
+
+    cookie_lifetime = (1, 12)
+
+    # allow only certain OP's .. with directed identities
+    openidrp_allowed_op = []
+
+    openidrp_allow_registration = True
+    openidrp_registration_url = '' # url to your provider's registration page
+
+    # configurable SREG request values
+    # possible values:
+    #     nickname, email, fullname, dob, gender, country, language, timezone
+    # match these up with OpenIDRP.auth_attribs
+    #     ['name', 'email', 'aliasname', 'language', 'tz_offset']
+    openidrp_sreg_required = ['nickname', 'email', 'timezone']
+    openidrp_sreg_optional = ['fullname', 'language']
+    openidrp_sreg_username_field = 'nickname' #'fullname'
+
+    # don't let users change password or have multiple openid's
+    user_form_disable = ['changepass', 'oid']
+
+    # remove some options from the large user preferences form
+    user_form_remove = ['css_url', 'quicklinks'] #'password', 'password1', 'password2']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/config/more_samples/openid_teams_wikiconfig_snippet	Wed Jul 22 21:50:03 2009 +0200
@@ -0,0 +1,29 @@
+    # This is a sample configuration snippet that shows moin's openid teams configuration
+    # See HelpOnOpenID, HelpOnAuthentication and HelpOnConfiguration for more info.
+
+    from MoinMoin.auth.openidrp import OpenIDAuth
+    from MoinMoin.auth.openidrp_ext.openidrp_teams import *
+
+    auth = [
+        OpenIDAuth(modify_request=openidrp_teams_modify_request,
+                   update_user=openidrp_teams_update_user,
+                   create_user=openidrp_teams_create_user),
+        # other auth methods can go here
+        #MoinAuth()
+    ]
+
+    cookie_lifetime = (1, 12)
+
+    # Launchpad Teams configuration
+    # list all teams you want to grant access to the wiki
+    openidrp_authorized_teams = ['team1', 'team2']
+
+    # ACL configuration, based on Teams
+    DesktopEdition = False
+    openidrp_acl_admin = 'AclAdmin'
+    openidrp_acl_page_postfix = 'Team'
+    acl_rights_default = u'Known:read,write All:read' #,write,delete,revert,admin"
+    acl_rights_before = u'%s:read,write,delete,revert,admin' % openidrp_acl_admin
+    acl_hierarchic = True
+    page_group_regex = ur'(?P<all>(?P<key>\S+)%s)' % openidrp_acl_page_postfix
+