changeset 840:faebfa285206

Merge with main
author Franz Pletz <fpletz AT franz-pletz DOT org>
date Sun, 11 Jun 2006 11:04:38 +0200
parents 4562cd3a4a5f (current diff) 9d74a2f53323 (diff)
children 3157ddeb3667
files MoinMoin/Page.py MoinMoin/PageEditor.py MoinMoin/multiconfig.py docs/CHANGES setup.py
diffstat 21 files changed, 796 insertions(+), 595 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/Page.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/Page.py	Sun Jun 11 11:04:38 2006 +0200
@@ -2,7 +2,7 @@
 """
     MoinMoin - Page class
 
-    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>
+    @copyright: 2000-2004 by Jrgen Hermann <jh@web.de>
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -1681,4 +1681,23 @@
         text = text.replace(u'\r', u'')
         return text
 
+    def isConflict(self):
+        """ Returns true if there is a known editing conflict for that page.
+        
+        @return: true if there is a known conflict.
+        """
 
+        cache = caching.CacheEntry(self.request, self, 'conflict', scope='item')
+        return cache.exists()
+    
+    def setConflict(self, state):
+        """ Sets the editing conflict flag.
+        
+        @param state: bool, true if there is a conflict.
+        """
+        
+        cache = caching.CacheEntry(self.request, self, 'conflict', scope='item')
+        if state:
+            cache.update("") # touch it!
+        else:
+            cache.remove()
--- a/MoinMoin/PageEditor.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/PageEditor.py	Sun Jun 11 11:04:38 2006 +0200
@@ -2,7 +2,7 @@
 """
     MoinMoin - PageEditor class
 
-    @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>
+    @copyright: 2000-2004 by Jrgen Hermann <jh@web.de>
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -109,6 +109,7 @@
             self.set_raw_body(verynewtext)
             return True
 
+        # this should never happen, except for empty pages
         return False
 
     def sendconfirmleaving(self):
@@ -217,11 +218,7 @@
                 conflict_msg = _('Someone else changed this page while you were editing!')
                 if self.mergeEditConflict(rev):
                     conflict_msg = _("""Someone else saved this page while you were editing!
-Please review the page and save then. Do not save this page as it is!
-Have a look at the diff of %(difflink)s to see what has been changed.""") % {
-                        'difflink': self.link_to(self.request,
-                                                 querystr='action=diff&rev=%d' % rev)
-                        }
+Please review the page and save then. Do not save this page as it is!""") 
                     rev = self.current_rev()
             if conflict_msg:
                 # We don't show preview when in conflict
@@ -923,17 +920,12 @@
                 if newtext==saved_page.get_raw_body():
                     msg = _("You already saved this page!")
                     return msg
-                
-            msg = _("""Sorry, someone else saved the page while you edited it.
+                else:
+                    msg = _("You already edited this page! Please do not use the back button.")
+                    raise self.EditConflict, msg
 
-Please do the following: Use the back button of your browser, and cut&paste
-your changes from there. Then go forward to here, and click EditText again.
-Now re-add your changes to the current page contents.
-
-''Do not just replace
-the content editbox with your version of the page, because that would
-delete the changes of the other person, which is excessively rude!''
-""")
+                msg = _("""Someone else saved this page while you were editing!
+Please review the page and save then. Do not save this page as it is!""") 
 
             if backup_url:
                 msg += "<p>%s</p>" % _(
--- a/MoinMoin/PageGraphicalEditor.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/PageGraphicalEditor.py	Sun Jun 11 11:04:38 2006 +0200
@@ -123,11 +123,7 @@
                 conflict_msg = _('Someone else changed this page while you were editing!')
                 if self.mergeEditConflict(rev):
                     conflict_msg = _("""Someone else saved this page while you were editing!
-Please review the page and save then. Do not save this page as it is!
-Have a look at the diff of %(difflink)s to see what has been changed.""") % {
-                        'difflink': self.link_to(self.request,
-                                                 querystr='action=diff&rev=%d' % rev)
-                        }
+Please review the page and save then. Do not save this page as it is!""")
                     rev = self.current_rev()
             if conflict_msg:
                 # We don't show preview when in conflict
@@ -140,6 +136,8 @@
             # page creation
             rev = 0
 
+        self.setConflict(bool(conflict_msg))
+        
         # Page editing is done using user language
         self.request.setContentLanguage(self.request.lang)
 
--- a/MoinMoin/action/__init__.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/action/__init__.py	Sun Jun 11 11:04:38 2006 +0200
@@ -427,26 +427,19 @@
     # Save new text
     else:
         try:
+            still_conflict = "/!\ '''Edit conflict" in savetext
+            pg.setConflict(still_conflict)
             savemsg = pg.saveText(savetext, rev, trivial=trivial, comment=comment)
-        except pg.EditConflict, msg:
+        except pg.EditConflict, e:
+            msg = e.message
+
             # Handle conflict and send editor
-
-            # TODO: conflict messages are duplicated from PageEditor,
-            # refactor to one place only.
-            conflict_msg = _('Someone else changed this page while you were editing!')
             pg.set_raw_body(savetext, modified=1)
-            if pg.mergeEditConflict(rev):
-                conflict_msg = _("""Someone else saved this page while you were editing!
-Please review the page and save then. Do not save this page as it is!
-Have a look at the diff of %(difflink)s to see what has been changed.""") % {
-                    'difflink': pg.link_to(pg.request,
-                                           querystr='action=diff&rev=%d' % rev)
-                    }
-                # We don't send preview when we do merge conflict
-                pg.sendEditor(msg=conflict_msg, comment=comment)
-                return
-            else:
-                savemsg = conflict_msg
+
+            pg.mergeEditConflict(rev)
+            # We don't send preview when we do merge conflict
+            pg.sendEditor(msg=msg, comment=comment)
+            return
         
         except pg.SaveError, msg:
             # msg contain a unicode string
--- a/MoinMoin/auth/__init__.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/auth/__init__.py	Sun Jun 11 11:04:38 2006 +0200
@@ -36,11 +36,11 @@
     method that authentified the user.
 
     TODO: check against other cookie work (see wiki)  
-          kill unsecure MOIN_ID cookie?
           reduce amount of XXX
           
-    @copyright: 2005-2006 Bastian Blank, Florian Festi, Thomas Waldmann
-    @copyright: 2005-2006 MoinMoin:AlexanderSchremmer
+    @copyright: 2005-2006 Bastian Blank, Florian Festi, MoinMoin:ThomasWaldmann,
+                          MoinMoin:AlexanderSchremmer, Nick Phillips,
+                          MoinMoin:FrankieChow, MoinMoin:NirSoffer
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -49,19 +49,21 @@
 
 # cookie names
 MOIN_SESSION = 'MOIN_SESSION'
-MOIN_ID = 'MOIN_ID'
 
-def log(request, **kw):
-    """ just log the call, do nothing else """
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-    request.log("auth.log: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
-    return user_obj, True
+import hmac, random
 
-# some cookie functions used by moin_cookie and moin_session auth
+def generate_security_string(length):
+    """ generate a random length (length/2 .. length) string with random content """
+    random_length = random.randint(length/2, length)
+    safe = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'
+    return ''.join([random.choice(safe) for i in range(random_length)])
+
+def make_security_hash(request, data, securitystring=''):
+    """ generate a hash string based on site configuration's cfg.cookie_secret,
+        securitystring and the data.
+    """
+    return hmac.new(request.cfg.cookie_secret + securitystring, data).hexdigest()
+
 def makeCookie(request, cookie_name, cookie_string, maxage, expires):
     """ create an appropriate cookie """
     c = Cookie.SimpleCookie()
@@ -81,7 +83,7 @@
     c[cookie_name]['expires'] = request.httpDate(when=expires, rfc='850')        
     return c.output()
 
-def setCookie(request, u, cookie_name=MOIN_ID, cookie_string=None):
+def setCookie(request, u, cookie_name, cookie_string):
     """ Set cookie for the user obj u
     
     cfg.cookie_lifetime and the user 'remember_me' setting set the
@@ -93,10 +95,6 @@
      > 0    n hours, or forever if user checked 'remember_me'
      < 0    -n hours, ignoring user 'remember_me' setting
     """
-    if cookie_string is None:
-        # For moin_cookie
-        cookie_string = u.id
-    
     # Calculate cookie maxage and expires
     lifetime = int(request.cfg.cookie_lifetime) * 3600 
     forever = 10 * 365 * 24 * 3600 # 10 years
@@ -119,20 +117,18 @@
     request.disableHttpCaching()
 
 def setSessionCookie(request, u):
-    """ Set moin_session cookie for user obj u
-    """
-    import base64, hmac
+    """ Set moin_session cookie for user obj u """
+    import base64
     cfg = request.cfg
-    cookie_name = MOIN_SESSION
     enc_username = base64.encodestring(u.auth_username)
     enc_id = base64.encodestring(u.id)
     # XXX - should include expiry!
     cookie_body = "username=%s:id=%s" % (enc_username, enc_id)
-    cookie_hmac = hmac.new(cfg.cookie_secret, cookie_body).hexdigest()
-    cookie_string = ':'.join([cookie_hmac, cookie_body])
-    setCookie(request, u, cookie_name, cookie_string)
+    cookie_hash = make_security_hash(request, cookie_body)
+    cookie_string = ':'.join([cookie_hash, cookie_body])
+    setCookie(request, u, MOIN_SESSION, cookie_string)
 
-def deleteCookie(request, cookie_name=MOIN_ID):
+def deleteCookie(request, cookie_name):
     """ Delete the user cookie by sending expired cookie with null value
 
     According to http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.2
@@ -151,62 +147,39 @@
     # IMPORTANT: Prevent caching of current page and cookie        
     request.disableHttpCaching()
 
-def moin_cookie(request, **kw):
-    """ authenticate via the MOIN_ID cookie """
+def moin_login(request, **kw):
+    """ handle login from moin login form, session has to be established later by moin_session """
     username = kw.get('name')
     password = kw.get('password')
     login = kw.get('login')
-    logout = kw.get('logout')
+    #logout = kw.get('logout')
     user_obj = kw.get('user_obj')
 
     cfg = request.cfg
     verbose = False
-    if hasattr(cfg, 'moin_cookie_verbose'):
-        verbose = cfg.moin_cookie_verbose
+    if hasattr(cfg, 'moin_login_verbose'):
+        verbose = cfg.moin_login_verbose
     
-    #request.log("auth.moin_cookie: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
+    #request.log("auth.moin_login: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
 
     if login:
-        if verbose: request.log("moin_cookie performing login action")
-        u = user.User(request, name=username, password=password,
-                      auth_method='login_userpassword')
+        if verbose: request.log("moin_login performing login action")
+        u = user.User(request, name=username, password=password, auth_method='moin_login')
         if u.valid:
-            if verbose: request.log("moin_cookie got valid user...")
-            setCookie(request, u)
-            return u, True # we make continuing possible, e.g. for smbmount
-        if verbose: request.log("moin_cookie not valid, previous valid=%d." % user_obj.valid)
-        return user_obj, True
+            if verbose: request.log("moin_login got valid user...")
+            user_obj = u
+        else:
+            if verbose: request.log("moin_login not valid, previous valid=%d." % user_obj.valid)
 
-    try:
-        cookie = Cookie.SimpleCookie(request.saved_cookie)
-    except Cookie.CookieError:
-        # ignore invalid cookies, else user can't relogin
-        cookie = None
-    if cookie and cookie.has_key(MOIN_ID):
-        u = user.User(request, id=cookie[MOIN_ID].value,
-                      auth_method='moin_cookie', auth_attribs=())
-
-        if logout:
-            u.valid = 0 # just make user invalid, but remember him
-
-        if u.valid:
-            setCookie(request, u) # refreshes cookie lifetime
-            return u, True # use True to get other methods called, too
-        else: # logout or invalid user
-            deleteCookie(request)
-            return u, True # we return a invalidated user object, so that
-                           # following auth methods can get the name of
-                           # the user who logged out
     return user_obj, True
 
-
 def moin_session(request, **kw):
     """ Authenticate via cookie.
     
     We don't handle initial logins (except to set the appropriate cookie), just
     ongoing sessions, and logout. Use another method for initial login.
     """
-    import hmac, base64
+    import base64
     
     username = kw.get('name')
     login = kw.get('login')
@@ -252,16 +225,16 @@
         return user_obj, True
     
     try:
-        cookie_hmac, cookie_body = cookie[cookie_name].value.split(':', 1)
+        cookie_hash, cookie_body = cookie[cookie_name].value.split(':', 1)
     except ValueError:
         # Invalid cookie
         if verbose: request.log("invalid cookie format: (%s)" % cookie[cookie_name].value)
         return user_obj, True
     
-    if cookie_hmac != hmac.new(cfg.cookie_secret, cookie_body).hexdigest():
+    if cookie_hash != make_security_hash(request, cookie_body):
         # Invalid cookie
         # XXX Cookie clear here???
-        if verbose: request.log("cookie recovered had invalid hmac")
+        if verbose: request.log("cookie recovered had invalid hash")
         return user_obj, True
 
     # We can trust the cookie
@@ -289,422 +262,3 @@
     setSessionCookie(request, u) # refreshes cookie lifetime
     return u, True # use True to get other methods called, too
 
-
-def http(request, **kw):
-    """ authenticate via http basic/digest/ntlm auth """
-    from MoinMoin.request import TWISTED, CLI
-    user_obj = kw.get('user_obj')
-    u = None
-    # check if we are running Twisted
-    if isinstance(request, TWISTED.Request):
-        username = request.twistd.getUser()
-        password = request.twistd.getPassword()
-        # when using Twisted http auth, we use username and password from
-        # the moin user profile, so both can be changed by user.
-        u = user.User(request, auth_username=username, password=password,
-                      auth_method='http', auth_attribs=())
-
-    elif not isinstance(request, CLI.Request):
-        env = request.env
-        auth_type = env.get('AUTH_TYPE','')
-        if auth_type in ['Basic', 'Digest', 'NTLM', 'Negotiate',]:
-            username = env.get('REMOTE_USER','')
-            if auth_type in ('NTLM', 'Negotiate',):
-                # converting to standard case so the user can even enter wrong case
-                # (added since windows does not distinguish between e.g.
-                #  "Mike" and "mike")
-                username = username.split('\\')[-1] # split off domain e.g.
-                                                    # from DOMAIN\user
-                # this "normalizes" the login name from {meier, Meier, MEIER} to Meier
-                # put a comment sign in front of next line if you don't want that:
-                username = username.title()
-            # when using http auth, we have external user name and password,
-            # we don't use the moin user profile for those attributes.
-            u = user.User(request, auth_username=username,
-                          auth_method='http', auth_attribs=('name', 'password'))
-
-    if u:
-        u.create_or_update()
-    if u and u.valid:
-        return u, True # True to get other methods called, too
-    else:
-        return user_obj, True
-
-def sslclientcert(request, **kw):
-    """ authenticate via SSL client certificate """
-    from MoinMoin.request import TWISTED
-    user_obj = kw.get('user_obj')
-    u = None
-    changed = False
-    # check if we are running Twisted
-    if isinstance(request, TWISTED.Request):
-        return user_obj, True # not supported if we run twisted
-        # Addendum: this seems to need quite some twisted insight and coding.
-        # A pointer i got on #twisted: divmod's vertex.sslverify
-        # If you really need this, feel free to implement and test it and
-        # submit a patch if it works.
-    else:
-        env = request.env
-        if env.get('SSL_CLIENT_VERIFY', 'FAILURE') == 'SUCCESS':
-            # if we only want to accept some specific CA, do a check like:
-            # if env.get('SSL_CLIENT_I_DN_OU') == "http://www.cacert.org"
-            email = env.get('SSL_CLIENT_S_DN_Email', '')
-            email_lower = email.lower()
-            commonname = env.get('SSL_CLIENT_S_DN_CN', '')
-            commonname_lower = commonname.lower()
-            if email_lower or commonname_lower:
-                for uid in user.getUserList(request):
-                    u = user.User(request, uid,
-                                  auth_method='sslclientcert', auth_attribs=())
-                    if email_lower and u.email.lower() == email_lower:
-                        u.auth_attribs = ('email', 'password')
-                        #this is only useful if same name should be used, as
-                        #commonname is likely no CamelCase WikiName
-                        #if commonname_lower != u.name.lower():
-                        #    u.name = commonname
-                        #    changed = True
-                        #u.auth_attribs = ('email', 'name', 'password')
-                        break
-                    if commonname_lower and u.name.lower() == commonname_lower:
-                        u.auth_attribs = ('name', 'password')
-                        #this is only useful if same email should be used as
-                        #specified in certificate.
-                        #if email_lower != u.email.lower():
-                        #    u.email = email
-                        #    changed = True
-                        #u.auth_attribs = ('name', 'email', 'password')
-                        break
-                else:
-                    u = None
-                if u is None:
-                    # user wasn't found, so let's create a new user object
-                    u = user.User(request, name=commonname_lower, auth_username=commonname_lower)
-
-    if u:
-        u.create_or_update(changed)
-    if u and u.valid:
-        return u, True
-    else:
-        return user_obj, True
-
-
-def smb_mount(request, **kw):
-    """ (u)mount a SMB server's share for username (using username/password for
-        authentication at the SMB server). This can be used if you need access
-        to files on some share via the wiki, but needs more code to be useful.
-        If you don't need it, don't use it.
-    """
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-    cfg = request.cfg
-    verbose = cfg.smb_verbose
-    if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
-    
-    # we just intercept login to mount and logout to umount the smb share
-    if login or logout:
-        import os, pwd, subprocess
-        web_username = cfg.smb_dir_user
-        web_uid = pwd.getpwnam(web_username)[2] # XXX better just use current uid?
-        if logout and user_obj: # logout -> we don't have username in form
-            username = user_obj.name # so we take it from previous auth method (moin_cookie e.g.)
-        mountpoint = cfg.smb_mountpoint % {
-            'username': username,
-        }
-        if login:
-            cmd = u"sudo mount -t cifs -o user=%(user)s,domain=%(domain)s,uid=%(uid)d,dir_mode=%(dir_mode)s,file_mode=%(file_mode)s,iocharset=%(iocharset)s //%(server)s/%(share)s %(mountpoint)s >>%(log)s 2>&1"
-        elif logout:
-            cmd = u"sudo umount %(mountpoint)s >>%(log)s 2>&1"
-            
-        cmd = cmd % {
-            'user': username,
-            'uid': web_uid,
-            'domain': cfg.smb_domain,
-            'server': cfg.smb_server,
-            'share': cfg.smb_share,
-            'mountpoint': mountpoint,
-            'dir_mode': cfg.smb_dir_mode,
-            'file_mode': cfg.smb_file_mode,
-            'iocharset': cfg.smb_iocharset,
-            'log': cfg.smb_log,
-        }
-        env = os.environ.copy()
-        if login:
-            try:
-                os.makedirs(mountpoint) # the dir containing the mountpoint must be writeable for us!
-            except OSError, err:
-                pass
-            env['PASSWD'] = password.encode(cfg.smb_coding)
-        subprocess.call(cmd.encode(cfg.smb_coding), env=env, shell=True)
-    return user_obj, True
-
-
-def ldap_login(request, **kw):
-    """ get authentication data from form, authenticate against LDAP (or Active Directory),
-        fetch some user infos from LDAP and create a user profile for that user that must
-        be used by subsequent auth plugins (like moin_cookie) as we never return a user
-        object from ldap_login.
-    """
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-
-    cfg = request.cfg
-    verbose = cfg.ldap_verbose
-    
-    if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
-    
-    # we just intercept login and logout for ldap, other requests have to be
-    # handled by another auth handler
-    if not login and not logout:
-        return user_obj, True
-    
-    import sys, re
-    import ldap
-    import traceback
-
-    u = None
-    coding = cfg.ldap_coding
-    try:
-        if verbose: request.log("LDAP: Trying to initialize %s." % cfg.ldap_uri)
-        l = ldap.initialize(cfg.ldap_uri)
-        if verbose: request.log("LDAP: Connected to LDAP server %s." % cfg.ldap_uri)
-        # you can use %(username)s and %(password)s here to get the stuff entered in the form:
-        ldap_binddn = cfg.ldap_binddn % locals()
-        ldap_bindpw = cfg.ldap_bindpw % locals()
-        l.simple_bind_s(ldap_binddn.encode(coding), ldap_bindpw.encode(coding))
-        if verbose: request.log("LDAP: Bound with binddn %s" % ldap_binddn)
-
-        filterstr = "(%s=%s)" % (cfg.ldap_name_attribute, username)
-        if verbose: request.log("LDAP: Searching %s" % filterstr)
-        lusers = l.search_st(cfg.ldap_base, cfg.ldap_scope,
-                             filterstr.encode(coding), timeout=cfg.ldap_timeout)
-        result_length = len(lusers)
-        if result_length != 1:
-            if result_length > 1:
-                request.log("LDAP: Search found more than one (%d) matches for %s." % (len(lusers), filterstr))
-            if result_length == 0:
-                if verbose: request.log("LDAP: Search found no matches for %s." % (filterstr, ))
-            return user_obj, True
-
-        dn, ldap_dict = lusers[0]
-        if verbose:
-            request.log("LDAP: debug lusers = %r" % lusers)
-            for key,val in ldap_dict.items():
-                request.log("LDAP: %s: %s" % (key, val))
-
-        try:
-            if verbose: request.log("LDAP: DN found is %s, trying to bind with pw" % dn)
-            l.simple_bind_s(dn, password.encode(coding))
-            if verbose: request.log("LDAP: Bound with dn %s (username: %s)" % (dn, username))
-            
-            email = ldap_dict.get(cfg.ldap_email_attribute, [''])[0]
-            email = email.decode(coding)
-            sn, gn = ldap_dict.get('sn', [''])[0], ldap_dict.get('givenName', [''])[0]
-            aliasname = ''
-            if sn and gn:
-                aliasname = "%s, %s" % (sn, gn)
-            elif sn:
-                aliasname = sn
-            aliasname = aliasname.decode(coding)
-            
-            u = user.User(request, auth_username=username, password="{SHA}NotStored", auth_method='ldap', auth_attribs=('name', 'password', 'email', 'mailto_author',))
-            u.name = username
-            u.aliasname = aliasname
-            u.email = email
-            u.remember_me = 0 # 0 enforces cookie_lifetime config param
-            if verbose: request.log("LDAP: creating userprefs with name %s email %s alias %s" % (username, email, aliasname))
-            
-        except ldap.INVALID_CREDENTIALS, err:
-            request.log("LDAP: invalid credentials (wrong password?) for dn %s (username: %s)" % (dn, username))
-
-    except:
-        info = sys.exc_info()
-        request.log("LDAP: caught an exception, traceback follows...")
-        request.log(''.join(traceback.format_exception(*info)))
-
-    if u:
-        u.create_or_update(True)
-    return u, True # moin_cookie has to set the cookie and return the user obj
-
-
-def interwiki(request, **kw):
-    # TODO use auth_method and auth_attribs for User object
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-
-    if login:
-        wikitag, wikiurl, wikitail, err = wikiutil.resolve_wiki(username)
-
-        if err or wikitag not in request.cfg.trusted_wikis:
-            return user_obj, True
-        
-        if password:
-            import xmlrpclib
-            homewiki = xmlrpclib.Server(wikiurl + "?action=xmlrpc2")
-            account_data = homewiki.getUser(wikitail, password)
-            if isinstance(account_data, str):
-                # show error message
-                return user_obj, True
-            
-            u = user.User(request, name=username)
-            for key, value in account_data.iteritems():
-                if key not in ["may", "id", "valid", "trusted"
-                               "auth_username",
-                               "name", "aliasname",
-                               "enc_passwd"]:
-                    setattr(u, key, value)
-            u.save()
-            setCookie(request, u)
-            return u, True
-        else:
-            pass
-            # XXX redirect to homewiki
-    
-    return user_obj, True
-
-
-class php_session:
-    """ Authentication module for PHP based frameworks
-        Authenticates via PHP session cookie. Currently supported systems:
-
-        * eGroupware 1.2 ("egw")
-         * You need to configure eGroupware in the "header setup" to use
-           "php sessions plus restore"
-
-        @copyright: 2005 by MoinMoin:AlexanderSchremmer
-            - Thanks to Spreadshirt
-    """
-
-    def __init__(self, apps=['egw'], s_path="/tmp", s_prefix="sess_"):
-        """ @param apps: A list of the enabled applications. See above for
-            possible keys.
-            @param s_path: The path where the PHP sessions are stored.
-            @param s_prefix: The prefix of the session files.
-        """
-        
-        self.s_path = s_path
-        self.s_prefix = s_prefix
-        self.apps = apps
-
-    def __call__(self, request, **kw):
-        def handle_egroupware(session):
-            """ Extracts name, fullname and email from the session. """
-            username = session['egw_session']['session_lid'].split("@", 1)[0]
-            known_accounts = session['egw_info_cache']['accounts']['cache']['account_data']
-            
-            # if the next line breaks, then the cache was not filled with the current
-            # user information
-            user_info = [value for key, value in known_accounts.items()
-                         if value['account_lid'] == username][0]
-            name = user_info.get('fullname', '')
-            email = user_info.get('email', '')
-            
-            dec = lambda x: x and x.decode("iso-8859-1")
-            
-            return dec(username), dec(email), dec(name)
-        
-        import Cookie, urllib
-        from MoinMoin.user import User
-        from MoinMoin.auth import _PHPsessionParser
-    
-        user_obj = kw.get('user_obj')
-        try:
-            cookie = Cookie.SimpleCookie(request.saved_cookie)
-        except Cookie.CookieError: # ignore invalid cookies
-            cookie = None
-        if cookie:
-            for cookiename in cookie.keys():
-                cookievalue = urllib.unquote(cookie[cookiename].value).decode('iso-8859-1')
-                session = _PHPsessionParser.loadSession(cookievalue, path=self.s_path, prefix=self.s_prefix)
-                if session:
-                    if "egw" in self.apps and session.get('egw_session', None):
-                        username, email, name = handle_egroupware(session)
-                        break
-            else:
-                return user_obj, True
-            
-            user = User(request, name=username, auth_username=username)
-            
-            changed = False
-            if name != user.aliasname:
-                user.aliasname = name
-                changed = True
-            if email != user.email:
-                user.email = email
-                changed = True
-            
-            if user:
-                user.create_or_update(changed)
-            if user and user.valid:
-                return user, True # True to get other methods called, too
-        return user_obj, True # continue with next method in auth list
-
-def mysql_group(request, **kw):
-    """ Authorize via MySQL group DB.
-    
-    We require an already-authenticated user_obj.
-    We don't worry about the type of request (login, logout, neither).
-    We just check user is part of authorized group.
-    """
-    import MySQLdb
-    
-    username = kw.get('name')
-#    login = kw.get('login')
-#    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-
-    cfg = request.cfg
-    verbose = False
-
-    if hasattr(cfg, 'mysql_group_verbose'):
-        verbose = cfg.mysql_group_verbose
-    
-    if verbose: request.log("auth.mysql_group: name=%s user_obj=%r" % (username, user_obj))
-
-    # Has any other method successfully authenticated?
-    if user_obj is not None and user_obj.valid:
-        # Yes - we can do stuff!
-        if verbose: request.log("mysql_group got valid user from previous auth method, trying authz...")
-        if verbose: request.log("mysql_group got auth_username %s." % user_obj.auth_username)
-
-        # XXX Check auth_username for dodgy chars (should be none as it is authenticated, but...)
-
-        # OK, now check mysql!
-        try:
-            m = MySQLdb.connect(host=cfg.mysql_group_dbhost,
-                                user=cfg.mysql_group_dbuser,
-                                passwd=cfg.mysql_group_dbpass,
-                                db=cfg.mysql_group_dbname,
-                                )
-        except:
-            import sys
-            import traceback
-            info = sys.exc_info()
-            request.log("mysql_group: authorization failed due to exception connecting to DB, traceback follows...")
-            request.log(''.join(traceback.format_exception(*info)))
-            return None, False
-        
-        c = m.cursor()
-        c.execute(cfg.mysql_group_query, user_obj.auth_username)
-        results = c.fetchall()
-        if results:
-            # Checked out OK
-            if verbose: request.log("mysql_group got %d results -- authorized!" % len(results))
-            return user_obj, True # we make continuing possible, e.g. for smbmount
-        else:
-            if verbose: request.log("mysql_group did not get match from DB -- not authorized")
-            return None, False
-    else:
-        # No other method succeeded, so we cannot authorize -- must fail
-        if verbose: request.log("mysql_group did not get valid user from previous auth method, cannot authorize")
-        return None, False
-
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/http.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,53 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - http authentication
+
+    You need either your webserver configured for doing HTTP auth (like Apache
+    reading some .htpasswd file) or Twisted (will accept HTTP auth against
+    password stored in moin user profile, but currently will NOT ask for auth).
+
+    @copyright: 2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin import user
+from MoinMoin.request import TWISTED, CLI
+
+def http(request, **kw):
+    """ authenticate via http basic/digest/ntlm auth """
+    user_obj = kw.get('user_obj')
+    u = None
+    # check if we are running Twisted
+    if isinstance(request, TWISTED.Request):
+        username = request.twistd.getUser()
+        password = request.twistd.getPassword()
+        # when using Twisted http auth, we use username and password from
+        # the moin user profile, so both can be changed by user.
+        u = user.User(request, auth_username=username, password=password,
+                      auth_method='http', auth_attribs=())
+
+    elif not isinstance(request, CLI.Request):
+        env = request.env
+        auth_type = env.get('AUTH_TYPE','')
+        if auth_type in ['Basic', 'Digest', 'NTLM', 'Negotiate',]:
+            username = env.get('REMOTE_USER','')
+            if auth_type in ('NTLM', 'Negotiate',):
+                # converting to standard case so the user can even enter wrong case
+                # (added since windows does not distinguish between e.g.
+                #  "Mike" and "mike")
+                username = username.split('\\')[-1] # split off domain e.g.
+                                                    # from DOMAIN\user
+                # this "normalizes" the login name from {meier, Meier, MEIER} to Meier
+                # put a comment sign in front of next line if you don't want that:
+                username = username.title()
+            # when using http auth, we have external user name and password,
+            # we don't use the moin user profile for those attributes.
+            u = user.User(request, auth_username=username,
+                          auth_method='http', auth_attribs=('name', 'password'))
+
+    if u:
+        u.create_or_update()
+    if u and u.valid:
+        return u, True # True to get other methods called, too
+    else:
+        return user_obj, True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/interwiki.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,50 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - authentication using a remote wiki
+
+    This is completely untested and rather has to be seen as an idea
+    than a working implementation.
+
+    @copyright: 2005 by ???
+    @license: GNU GPL, see COPYING for details.
+"""
+import xmlrpclib
+from MoinMoin import auth, wikiutil, user
+
+def interwiki(request, **kw):
+    # TODO use auth_method and auth_attribs for User object
+    username = kw.get('name')
+    password = kw.get('password')
+    login = kw.get('login')
+    logout = kw.get('logout')
+    user_obj = kw.get('user_obj')
+
+    if login:
+        wikitag, wikiurl, wikitail, err = wikiutil.resolve_wiki(username)
+
+        if err or wikitag not in request.cfg.trusted_wikis:
+            return user_obj, True
+        
+        if password:
+            homewiki = xmlrpclib.Server(wikiurl + "?action=xmlrpc2")
+            account_data = homewiki.getUser(wikitail, password)
+            if isinstance(account_data, str):
+                # show error message
+                return user_obj, True
+            
+            u = user.User(request, name=username)
+            for key, value in account_data.iteritems():
+                if key not in ["may", "id", "valid", "trusted"
+                               "auth_username",
+                               "name", "aliasname",
+                               "enc_passwd"]:
+                    setattr(u, key, value)
+            u.save()
+            auth.setSessionCookie(request, u)
+            return u, True
+        else:
+            pass
+            # XXX redirect to homewiki
+    
+    return user_obj, True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/ldap_login.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,101 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - LDAP / Active Directory authentication
+
+    This code only creates a user object, the session has to be established by
+    the auth.moin_session auth plugin.
+    
+    @copyright: 2006 by MoinMoin:ThomasWaldmann, Nick Phillips
+    @license: GNU GPL, see COPYING for details.
+"""
+import sys, re
+import traceback
+import ldap
+from MoinMoin import user
+
+def ldap_login(request, **kw):
+    """ get authentication data from form, authenticate against LDAP (or Active Directory),
+        fetch some user infos from LDAP and create a user profile for that user that must
+        be used by subsequent auth plugins (like moin_cookie) as we never return a user
+        object from ldap_login.
+    """
+    username = kw.get('name')
+    password = kw.get('password')
+    login = kw.get('login')
+    logout = kw.get('logout')
+    user_obj = kw.get('user_obj')
+
+    cfg = request.cfg
+    verbose = cfg.ldap_verbose
+    
+    if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
+    
+    # we just intercept login and logout for ldap, other requests have to be
+    # handled by another auth handler
+    if not login and not logout:
+        return user_obj, True
+    
+    u = None
+    coding = cfg.ldap_coding
+    try:
+        if verbose: request.log("LDAP: Trying to initialize %s." % cfg.ldap_uri)
+        l = ldap.initialize(cfg.ldap_uri)
+        if verbose: request.log("LDAP: Connected to LDAP server %s." % cfg.ldap_uri)
+        # you can use %(username)s and %(password)s here to get the stuff entered in the form:
+        ldap_binddn = cfg.ldap_binddn % locals()
+        ldap_bindpw = cfg.ldap_bindpw % locals()
+        l.simple_bind_s(ldap_binddn.encode(coding), ldap_bindpw.encode(coding))
+        if verbose: request.log("LDAP: Bound with binddn %s" % ldap_binddn)
+
+        filterstr = "(%s=%s)" % (cfg.ldap_name_attribute, username)
+        if verbose: request.log("LDAP: Searching %s" % filterstr)
+        lusers = l.search_st(cfg.ldap_base, cfg.ldap_scope,
+                             filterstr.encode(coding), timeout=cfg.ldap_timeout)
+        result_length = len(lusers)
+        if result_length != 1:
+            if result_length > 1:
+                request.log("LDAP: Search found more than one (%d) matches for %s." % (len(lusers), filterstr))
+            if result_length == 0:
+                if verbose: request.log("LDAP: Search found no matches for %s." % (filterstr, ))
+            return user_obj, True
+
+        dn, ldap_dict = lusers[0]
+        if verbose:
+            request.log("LDAP: debug lusers = %r" % lusers)
+            for key,val in ldap_dict.items():
+                request.log("LDAP: %s: %s" % (key, val))
+
+        try:
+            if verbose: request.log("LDAP: DN found is %s, trying to bind with pw" % dn)
+            l.simple_bind_s(dn, password.encode(coding))
+            if verbose: request.log("LDAP: Bound with dn %s (username: %s)" % (dn, username))
+            
+            email = ldap_dict.get(cfg.ldap_email_attribute, [''])[0]
+            email = email.decode(coding)
+            sn, gn = ldap_dict.get('sn', [''])[0], ldap_dict.get('givenName', [''])[0]
+            aliasname = ''
+            if sn and gn:
+                aliasname = "%s, %s" % (sn, gn)
+            elif sn:
+                aliasname = sn
+            aliasname = aliasname.decode(coding)
+            
+            u = user.User(request, auth_username=username, password="{SHA}NotStored", auth_method='ldap', auth_attribs=('name', 'password', 'email', 'mailto_author',))
+            u.name = username
+            u.aliasname = aliasname
+            u.email = email
+            u.remember_me = 0 # 0 enforces cookie_lifetime config param
+            if verbose: request.log("LDAP: creating userprefs with name %s email %s alias %s" % (username, email, aliasname))
+            
+        except ldap.INVALID_CREDENTIALS, err:
+            request.log("LDAP: invalid credentials (wrong password?) for dn %s (username: %s)" % (dn, username))
+
+    except:
+        info = sys.exc_info()
+        request.log("LDAP: caught an exception, traceback follows...")
+        request.log(''.join(traceback.format_exception(*info)))
+
+    if u:
+        u.create_or_update(True)
+    return u, True # moin_session has to set the cookie
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/log.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,21 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - logging auth plugin
+
+    This does nothing except logging the auth parameters (the password is NOT
+    logged, of course).
+
+    @copyright: 2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+def log(request, **kw):
+    """ just log the call, do nothing else """
+    username = kw.get('name')
+    password = kw.get('password')
+    login = kw.get('login')
+    logout = kw.get('logout')
+    user_obj = kw.get('user_obj')
+    request.log("auth.log: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
+    return user_obj, True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/mysql_group.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,71 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - auth plugin doing a check against MySQL group db
+
+    ...
+
+    @copyright: 2006 by Nick Phillips
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import MySQLdb
+
+def mysql_group(request, **kw):
+    """ Authorize via MySQL group DB.
+    
+    We require an already-authenticated user_obj.
+    We don't worry about the type of request (login, logout, neither).
+    We just check user is part of authorized group.
+    """
+    
+    username = kw.get('name')
+#    login = kw.get('login')
+#    logout = kw.get('logout')
+    user_obj = kw.get('user_obj')
+
+    cfg = request.cfg
+    verbose = False
+
+    if hasattr(cfg, 'mysql_group_verbose'):
+        verbose = cfg.mysql_group_verbose
+    
+    if verbose: request.log("auth.mysql_group: name=%s user_obj=%r" % (username, user_obj))
+
+    # Has any other method successfully authenticated?
+    if user_obj is not None and user_obj.valid:
+        # Yes - we can do stuff!
+        if verbose: request.log("mysql_group got valid user from previous auth method, trying authz...")
+        if verbose: request.log("mysql_group got auth_username %s." % user_obj.auth_username)
+
+        # XXX Check auth_username for dodgy chars (should be none as it is authenticated, but...)
+
+        # OK, now check mysql!
+        try:
+            m = MySQLdb.connect(host=cfg.mysql_group_dbhost,
+                                user=cfg.mysql_group_dbuser,
+                                passwd=cfg.mysql_group_dbpass,
+                                db=cfg.mysql_group_dbname,
+                                )
+        except:
+            import sys
+            import traceback
+            info = sys.exc_info()
+            request.log("mysql_group: authorization failed due to exception connecting to DB, traceback follows...")
+            request.log(''.join(traceback.format_exception(*info)))
+            return None, False
+        
+        c = m.cursor()
+        c.execute(cfg.mysql_group_query, user_obj.auth_username)
+        results = c.fetchall()
+        if results:
+            # Checked out OK
+            if verbose: request.log("mysql_group got %d results -- authorized!" % len(results))
+            return user_obj, True # we make continuing possible, e.g. for smbmount
+        else:
+            if verbose: request.log("mysql_group did not get match from DB -- not authorized")
+            return None, False
+    else:
+        # No other method succeeded, so we cannot authorize -- must fail
+        if verbose: request.log("mysql_group did not get valid user from previous auth method, cannot authorize")
+        return None, False
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/php_session.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,80 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - PHP session cookie authentication
+    
+    Currently supported systems:
+
+        * eGroupware 1.2 ("egw")
+         * You need to configure eGroupware in the "header setup" to use
+           "php sessions plus restore"
+
+    @copyright: 2005 by MoinMoin:AlexanderSchremmer (Thanks to Spreadshirt)
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import Cookie, urllib
+from MoinMoin import user
+from MoinMoin.auth import _PHPsessionParser
+
+class php_session:
+    """ PHP session cookie authentication """
+    def __init__(self, apps=['egw'], s_path="/tmp", s_prefix="sess_"):
+        """ @param apps: A list of the enabled applications. See above for
+            possible keys.
+            @param s_path: The path where the PHP sessions are stored.
+            @param s_prefix: The prefix of the session files.
+        """
+        
+        self.s_path = s_path
+        self.s_prefix = s_prefix
+        self.apps = apps
+
+    def __call__(self, request, **kw):
+        def handle_egroupware(session):
+            """ Extracts name, fullname and email from the session. """
+            username = session['egw_session']['session_lid'].split("@", 1)[0]
+            known_accounts = session['egw_info_cache']['accounts']['cache']['account_data']
+            
+            # if the next line breaks, then the cache was not filled with the current
+            # user information
+            user_info = [value for key, value in known_accounts.items()
+                         if value['account_lid'] == username][0]
+            name = user_info.get('fullname', '')
+            email = user_info.get('email', '')
+            
+            dec = lambda x: x and x.decode("iso-8859-1")
+            
+            return dec(username), dec(email), dec(name)
+        
+        user_obj = kw.get('user_obj')
+        try:
+            cookie = Cookie.SimpleCookie(request.saved_cookie)
+        except Cookie.CookieError: # ignore invalid cookies
+            cookie = None
+        if cookie:
+            for cookiename in cookie.keys():
+                cookievalue = urllib.unquote(cookie[cookiename].value).decode('iso-8859-1')
+                session = _PHPsessionParser.loadSession(cookievalue, path=self.s_path, prefix=self.s_prefix)
+                if session:
+                    if "egw" in self.apps and session.get('egw_session', None):
+                        username, email, name = handle_egroupware(session)
+                        break
+            else:
+                return user_obj, True
+            
+            user = user.User(request, name=username, auth_username=username)
+            
+            changed = False
+            if name != user.aliasname:
+                user.aliasname = name
+                changed = True
+            if email != user.email:
+                user.email = email
+                changed = True
+            
+            if user:
+                user.create_or_update(changed)
+            if user and user.valid:
+                return user, True # True to get other methods called, too
+        return user_obj, True # continue with next method in auth list
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/smb_mount.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,61 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - auth plugin for (un)mounting a smb share
+
+    (u)mount a SMB server's share for username (using username/password for
+    authentication at the SMB server). This can be used if you need access
+    to files on some share via the wiki, but needs more code to be useful.
+
+    @copyright: 2006 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+
+def smb_mount(request, **kw):
+    """ auth plugin for (un)mounting an smb share """
+    username = kw.get('name')
+    password = kw.get('password')
+    login = kw.get('login')
+    logout = kw.get('logout')
+    user_obj = kw.get('user_obj')
+    cfg = request.cfg
+    verbose = cfg.smb_verbose
+    if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
+    
+    # we just intercept login to mount and logout to umount the smb share
+    if login or logout:
+        import os, pwd, subprocess
+        web_username = cfg.smb_dir_user
+        web_uid = pwd.getpwnam(web_username)[2] # XXX better just use current uid?
+        if logout and user_obj: # logout -> we don't have username in form
+            username = user_obj.name # so we take it from previous auth method (moin_cookie e.g.)
+        mountpoint = cfg.smb_mountpoint % {
+            'username': username,
+        }
+        if login:
+            cmd = u"sudo mount -t cifs -o user=%(user)s,domain=%(domain)s,uid=%(uid)d,dir_mode=%(dir_mode)s,file_mode=%(file_mode)s,iocharset=%(iocharset)s //%(server)s/%(share)s %(mountpoint)s >>%(log)s 2>&1"
+        elif logout:
+            cmd = u"sudo umount %(mountpoint)s >>%(log)s 2>&1"
+            
+        cmd = cmd % {
+            'user': username,
+            'uid': web_uid,
+            'domain': cfg.smb_domain,
+            'server': cfg.smb_server,
+            'share': cfg.smb_share,
+            'mountpoint': mountpoint,
+            'dir_mode': cfg.smb_dir_mode,
+            'file_mode': cfg.smb_file_mode,
+            'iocharset': cfg.smb_iocharset,
+            'log': cfg.smb_log,
+        }
+        env = os.environ.copy()
+        if login:
+            try:
+                os.makedirs(mountpoint) # the dir containing the mountpoint must be writeable for us!
+            except OSError, err:
+                pass
+            env['PASSWD'] = password.encode(cfg.smb_coding)
+        subprocess.call(cmd.encode(cfg.smb_coding), env=env, shell=True)
+    return user_obj, True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/sslclientcert.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,71 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - SSL client certificate authentication
+
+    Currently not supported for Twisted web server, but only for web servers
+    setting SSL_CLIENT_* environment (e.g. Apache).
+    
+    @copyright: 2006 by MoinMoin:ThomasWaldmann,
+                2003 by Martin v. Löwis
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import user
+from MoinMoin.request import TWISTED
+
+def sslclientcert(request, **kw):
+    """ authenticate via SSL client certificate """
+    user_obj = kw.get('user_obj')
+    u = None
+    changed = False
+    # check if we are running Twisted
+    if isinstance(request, TWISTED.Request):
+        return user_obj, True # not supported if we run twisted
+        # Addendum: this seems to need quite some twisted insight and coding.
+        # A pointer i got on #twisted: divmod's vertex.sslverify
+        # If you really need this, feel free to implement and test it and
+        # submit a patch if it works.
+    else:
+        env = request.env
+        if env.get('SSL_CLIENT_VERIFY', 'FAILURE') == 'SUCCESS':
+            # if we only want to accept some specific CA, do a check like:
+            # if env.get('SSL_CLIENT_I_DN_OU') == "http://www.cacert.org"
+            email = env.get('SSL_CLIENT_S_DN_Email', '')
+            email_lower = email.lower()
+            commonname = env.get('SSL_CLIENT_S_DN_CN', '')
+            commonname_lower = commonname.lower()
+            if email_lower or commonname_lower:
+                for uid in user.getUserList(request):
+                    u = user.User(request, uid,
+                                  auth_method='sslclientcert', auth_attribs=())
+                    if email_lower and u.email.lower() == email_lower:
+                        u.auth_attribs = ('email', 'password')
+                        #this is only useful if same name should be used, as
+                        #commonname is likely no CamelCase WikiName
+                        #if commonname_lower != u.name.lower():
+                        #    u.name = commonname
+                        #    changed = True
+                        #u.auth_attribs = ('email', 'name', 'password')
+                        break
+                    if commonname_lower and u.name.lower() == commonname_lower:
+                        u.auth_attribs = ('name', 'password')
+                        #this is only useful if same email should be used as
+                        #specified in certificate.
+                        #if email_lower != u.email.lower():
+                        #    u.email = email
+                        #    changed = True
+                        #u.auth_attribs = ('name', 'email', 'password')
+                        break
+                else:
+                    u = None
+                if u is None:
+                    # user wasn't found, so let's create a new user object
+                    u = user.User(request, name=commonname_lower, auth_username=commonname_lower)
+
+    if u:
+        u.create_or_update(changed)
+    if u and u.valid:
+        return u, True
+    else:
+        return user_obj, True
+
--- a/MoinMoin/macro/RecentChanges.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/macro/RecentChanges.py	Sun Jun 11 11:04:38 2006 +0200
@@ -60,6 +60,12 @@
     if not page.exists():
         # indicate page was deleted
         html_link = request.theme.make_icon('deleted')
+    elif page.isConflict():
+        img = macro.formatter.smiley("/!\\")
+        #img = request.theme.make_icon('help')
+        html_link = wikiutil.link_tag(request,
+                                      wikiutil.quoteWikinameURL(pagename) + "?action=edit",
+                                      img, formatter=macro.formatter)
     elif is_new:
         # show "NEW" icon if page was created after the user's bookmark
         if hilite:
--- a/MoinMoin/multiconfig.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/multiconfig.py	Sun Jun 11 11:04:38 2006 +0200
@@ -173,7 +173,7 @@
     actions_excluded = [] # ['DeletePage', 'AttachFile', 'RenamePage', 'test', ]
     allow_xslt = 0
     attachments = None # {'dir': path, 'url': url-prefix}
-    auth = [authmodule.moin_cookie]
+    auth = [authmodule.moin_login, authmodule.moin_session,]
     
     backup_compression = 'gz'
     backup_users = []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/multicall.py	Sun Jun 11 11:04:38 2006 +0200
@@ -0,0 +1,70 @@
+""" XMLRPC MultiCall support for Python 2.3. Copied from xmlrpclib.py of Python 2.4.3. """
+
+try:
+    from xmlrpclib import MultiCall
+except ImportError: 
+    from xmlrpclib import Fault
+    
+    class _MultiCallMethod:
+        # some lesser magic to store calls made to a MultiCall object
+        # for batch execution
+        def __init__(self, call_list, name):
+            self.__call_list = call_list
+            self.__name = name
+        def __getattr__(self, name):
+            return _MultiCallMethod(self.__call_list, "%s.%s" % (self.__name, name))
+        def __call__(self, *args):
+            self.__call_list.append((self.__name, args))
+    
+    class MultiCallIterator:
+        """Iterates over the results of a multicall. Exceptions are
+        thrown in response to xmlrpc faults."""
+    
+        def __init__(self, results):
+            self.results = results
+    
+        def __getitem__(self, i):
+            item = self.results[i]
+            if type(item) == type({}):
+                raise Fault(item['faultCode'], item['faultString'])
+            elif type(item) == type([]):
+                return item[0]
+            else:
+                raise ValueError,\
+                      "unexpected type in multicall result"
+    
+    class MultiCall:
+        """server -> a object used to boxcar method calls
+    
+        server should be a ServerProxy object.
+    
+        Methods can be added to the MultiCall using normal
+        method call syntax e.g.:
+    
+        multicall = MultiCall(server_proxy)
+        multicall.add(2,3)
+        multicall.get_address("Guido")
+    
+        To execute the multicall, call the MultiCall object e.g.:
+    
+        add_result, address = multicall()
+        """
+    
+        def __init__(self, server):
+            self.__server = server
+            self.__call_list = []
+    
+        def __repr__(self):
+            return "<MultiCall at %x>" % id(self)
+    
+        __str__ = __repr__
+    
+        def __getattr__(self, name):
+            return _MultiCallMethod(self.__call_list, name)
+    
+        def __call__(self):
+            marshalled_list = []
+            for name, args in self.__call_list:
+                marshalled_list.append({'methodName' : name, 'params' : args})
+    
+            return MultiCallIterator(self.__server.system.multicall(marshalled_list))
--- a/MoinMoin/xmlrpc/__init__.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/MoinMoin/xmlrpc/__init__.py	Sun Jun 11 11:04:38 2006 +0200
@@ -5,7 +5,7 @@
     If you want to use wikirpc function "putPage", read the comments in
     xmlrpc_putPage or it won't work!
     
-    Parts of this code are based on Jürgen Hermann's wikirpc.py,
+    Parts of this code are based on Jrgen Hermann's wikirpc.py,
     Les Orchard's "xmlrpc.cgi" and further work by Gustavo Niemeyer.
 
     See http://www.ecyrd.com/JSPWiki/Wiki.jsp?page=WikiRPCInterface
@@ -108,6 +108,100 @@
             '\n'.join(traceback.format_tb(sys.exc_info()[2])),
         )
 
+    def process(self):
+        """ xmlrpc v1 and v2 dispatcher """
+        try:
+            data = self.request.read()
+            params, method = xmlrpclib.loads(data)
+    
+            if _debug:
+                sys.stderr.write('- XMLRPC ' + '-' * 70 + '\n')
+                sys.stderr.write('%s(%s)\n\n' % (method, repr(params)))
+            
+            response = self.dispatch(method, params)
+            
+        except:
+            # report exception back to server
+            response = xmlrpclib.dumps(xmlrpclib.Fault(1, self._dump_exc()))
+        else:
+            # wrap response in a singleton tuple
+            response = (response,)
+
+            # serialize it
+            response = xmlrpclib.dumps(response, methodresponse=1)
+
+        self.request.http_headers([
+            "Content-Type: text/xml;charset=utf-8",
+            "Content-Length: %d" % len(response),
+        ])
+        self.request.write(response)
+
+        if _debug:
+            sys.stderr.write('- XMLRPC ' + '-' * 70 + '\n')
+            sys.stderr.write(response + '\n\n')
+
+    def dispatch(self, method, params):
+        method = method.replace(".", "_")
+        
+        try:
+            fn = getattr(self, 'xmlrpc_' + method)
+        except AttributeError:
+            try:
+                fn = wikiutil.importPlugin(self.request.cfg, 'xmlrpc',
+                                           method, 'execute')
+            except wikiutil.PluginMissingError:
+                response = xmlrpclib.Fault(1, "No such method: %s." %
+                                           method)
+            else:
+                response = fn(self, *params)
+        else:
+            response = fn(*params)
+        
+        return response
+
+    # Common faults -----------------------------------------------------
+    
+    def notAllowedFault(self):
+        return xmlrpclib.Fault(1, "You are not allowed to read this page.")
+
+    def noSuchPageFault(self):
+        return xmlrpclib.Fault(1, "No such page was found.")        
+
+    #############################################################################
+    ### System methods
+    #############################################################################
+
+    def xmlrpc_system_multicall(self, call_list):
+        """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => [[4], ...]
+
+        Allows the caller to package multiple XML-RPC calls into a single
+        request.
+
+        See http://www.xmlrpc.com/discuss/msgReader$1208
+        
+        Copied from SimpleXMLRPCServer.py
+        """
+
+        results = []
+        for call in call_list:
+            method_name = call['methodName']
+            params = call['params']
+
+            try:
+                # XXX A marshalling error in any response will fail the entire
+                # multicall. If someone cares they should fix this.
+                results.append([self.dispatch(method_name, params)])
+            except xmlrpclib.Fault, fault:
+                results.append(
+                    {'faultCode' : fault.faultCode,
+                     'faultString' : fault.faultString}
+                    )
+            except:
+                results.append(
+                    {'faultCode' : 1,
+                     'faultString' : "%s:%s" % (sys.exc_type, sys.exc_value)}
+                    )
+        return results
 
     #############################################################################
     ### Interface implementation
@@ -398,6 +492,11 @@
                  self._outstr(results.formatContext(hit, 180, 1)))
                 for hit in results.hits]
 
+    def xmlrpc_getMoinVersion(self):
+        from MoinMoin import version
+        return (version.project, version.release, version.revision)
+
+
     # XXX BEGIN WARNING XXX
     # All xmlrpc_*Attachment* functions have to be considered as UNSTABLE API -
     # they are neither standard nor are they what we need when we have switched
@@ -471,57 +570,6 @@
     
     # XXX END WARNING XXX
 
-    def process(self):
-        """ xmlrpc v1 and v2 dispatcher """
-        try:
-            data = self.request.read()
-            params, method = xmlrpclib.loads(data)
-    
-            if _debug:
-                sys.stderr.write('- XMLRPC ' + '-' * 70 + '\n')
-                sys.stderr.write('%s(%s)\n\n' % (method, repr(params)))
-            
-            try:
-                fn = getattr(self, 'xmlrpc_' + method)
-            except AttributeError:
-                try:
-                    fn = wikiutil.importPlugin(self.request.cfg, 'xmlrpc',
-                                               method, 'execute')
-                except wikiutil.PluginMissingError:
-                    response = xmlrpclib.Fault(1, "No such method: %s." %
-                                               method)
-                else:
-                    response = fn(self, *params)
-            else:
-                response = fn(*params)
-        except:
-            # report exception back to server
-            response = xmlrpclib.dumps(xmlrpclib.Fault(1, self._dump_exc()))
-        else:
-            # wrap response in a singleton tuple
-            response = (response,)
-
-            # serialize it
-            response = xmlrpclib.dumps(response, methodresponse=1)
-
-        self.request.http_headers([
-            "Content-Type: text/xml;charset=utf-8",
-            "Content-Length: %d" % len(response),
-        ])
-        self.request.write(response)
-
-        if _debug:
-            sys.stderr.write('- XMLRPC ' + '-' * 70 + '\n')
-            sys.stderr.write(response + '\n\n')
-
-    # Common faults -----------------------------------------------------
-    
-    def notAllowedFault(self):
-        return xmlrpclib.Fault(1, "You are not allowed to read this page.")
-
-    def noSuchPageFault(self):
-        return xmlrpclib.Fault(1, "No such page was found.")        
-
 
 class XmlRpc1(XmlRpcBase):
     
--- a/docs/CHANGES	Sat Jun 10 15:29:31 2006 +0200
+++ b/docs/CHANGES	Sun Jun 11 11:04:38 2006 +0200
@@ -92,7 +92,9 @@
     * moved mailimport.py to mail/mailimport.py,
       moved util/mail.py to mail/sendmail.py
     * moved auth.py to auth/__init__.py,
-      moved util/sessionParser.py to auth/_PHPsessionParser.py
+      moved util/sessionParser.py to auth/_PHPsessionParser.py,
+      teared auth code into single modules under auth/* - moin_session handling
+      and the builting moin_login method are in auth/__init__.py.
     * added wikiutil.MimeType class (works internally with sanitized mime
       types because the official ones suck)
     * renamed parsers to module names representing sane mimetypes, e.g.:
--- a/docs/CHANGES.aschremmer	Sat Jun 10 15:29:31 2006 +0200
+++ b/docs/CHANGES.aschremmer	Sun Jun 11 11:04:38 2006 +0200
@@ -1,6 +1,3 @@
-Please use your CHANGES.$yourname for recording your changes you do while
-Google Summer of Code.
-
 Branch moin/1.6-sync-aschremmer
 ===============================
 
@@ -8,26 +5,28 @@
     * ...
 
   ToDo:
-    * ...
+    * Implement actual syncronisation.
+    * Implement a cross-site authentication system.
 
   New Features:
-    * ...
-  
+    * XMLRPC method to return the Moin version
+    * XMLRPC multicall support
+    * Conflict icon in RecentChanges
+
   Bugfixes (only stuff that is buggy in moin/1.6 main branch):
     * ...
 
   Other Changes:
-    * ...
-  
+    * Refactored conflict resolution and XMLRPC code.
+
   Developer notes:
     * ...
 
 
 Diary
 =====
-Please make at least one entry per day (and commit it) about what your work was about.
 
-2006-05-29 ...
-2006-05-30 ...
-2006-05-31 ...
+Week 21: Basic Infrastructur setup (repos), initial talks to the mentor, started writing the design document, helped other students to get started, started evaluating Mercurial as a DVCS backend
+Week 22: Tax forms, Fulfilled transcription request, written conflict icon support, refactored conflict handling, changed conflict icon, Added xmlrpc multicall support into the server and backported the client code from python 2.4
 
+
--- a/moin.spec	Sat Jun 10 15:29:31 2006 +0200
+++ b/moin.spec	Sun Jun 11 11:04:38 2006 +0200
@@ -44,6 +44,7 @@
 
 %prep
 %setup
+echo $RPM_BUILD_ROOT
 
 %build
 # replace python by python2 if python refers to version 1.5 on your system
@@ -52,15 +53,22 @@
 %install
 # replace python by python2 if python refers to version 1.5 on your system
 python setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
-
 %clean
 rm -rf $RPM_BUILD_ROOT
 
-%files -f INSTALLED_FILES
+#%files -f INSTALLED_FILES   # Wrong: Installed files contains directories also
+# This lets rpmbuild complain about Files listet twice.
+# A Good explanation is here: "http://www.wideopen.com/archives/rpm-list/2001-February/msg00081.html
+%files
 %defattr(-,root,root)
+/usr
 %doc  README docs/CHANGES docs/INSTALL.html docs/licenses/COPYING
 
 %changelog
+* Do Jun  8 2006 Johannes Poehlmann
+- Fix RPM build errror "Files listet twice" 
+  Replaced files list and just package all of /usr.
+
 * Fri Mar 05 2004 Florian Festi
 - Initial RPM release.
 
--- a/setup.py	Sat Jun 10 15:29:31 2006 +0200
+++ b/setup.py	Sun Jun 11 11:04:38 2006 +0200
@@ -137,6 +137,7 @@
                 'python': os.path.normpath(sys.executable),
                 'package': self.package_name,
                 'module': module,
+                'package_location': '/usr/lib/python/site-packages', # FIXME
             }
 
             self.announce("creating %s" % outfile)
@@ -149,9 +150,12 @@
                         'if     "%%_4ver%%" == "" %(python)s -c "from %(package)s.script.%(module)s import run; run()" %%*\n'
                         % script_vars)
                 else:
-                    file.write('#! %(python)s\n'
-                        'from %(package)s.script.%(module)s import run\n'
-                        'run()\n'
+                    file.write("#! %(python)s\n"
+                        "#Fix and uncomment those 2 lines if your moin command doesn't find the MoinMoin package:\n"
+                        "#import sys\n"
+                        "#sys.path.insert(0, '%(package_location)s')\n"
+                        "from %(package)s.script.%(module)s import run\n"
+                        "run()\n"
                         % script_vars)
             finally:
                 file.close()