changeset 3020:945368f271ef

Merge with main.
author Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl>
date Sun, 06 Jan 2008 23:39:24 +0100
parents d860ab45d438 (current diff) 5dfd26496da8 (diff)
children 313a1d430bcb
files wiki/server/moinwsgi.py
diffstat 46 files changed, 936 insertions(+), 334 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun Jan 06 23:38:45 2008 +0100
+++ b/Makefile	Sun Jan 06 23:39:24 2008 +0100
@@ -13,9 +13,9 @@
 
 install-docs:
 	-mkdir build
-	wget -U MoinMoin/Makefile -O build/INSTALL.html "http://moinmaster.wikiwikiweb.de/MoinMoin/InstallDocs?action=print"
+	wget -U MoinMoin/Makefile -O build/INSTALL.html "http://master.moinmo.in/MoinMoin/InstallDocs?action=print"
 	sed \
-		-e 's#href="/#href="http://moinmaster.wikiwikiweb.de/#g' \
+		-e 's#href="/#href="http://master.moinmo.in/#g' \
 		-e 's#http://[a-z\.]*/wiki/classic/#/wiki/classic/#g' \
 		-e 's#http://[a-z\.]*/wiki/modern/#/wiki/modern/#g' \
 		-e 's#http://[a-z\.]*/wiki/rightsidebar/#/wiki/rightsidebar/#g' \
@@ -28,7 +28,7 @@
 	-rmdir build
 
 interwiki:
-	wget -U MoinMoin/Makefile -O $(share)/data/intermap.txt "http://moinmaster.wikiwikiweb.de/InterWikiMap?action=raw"
+	wget -U MoinMoin/Makefile -O $(share)/data/intermap.txt "http://master.moinmo.in/InterWikiMap?action=raw"
 	chmod 664 $(share)/data/intermap.txt
 
 check-tabs:
@@ -42,8 +42,8 @@
 # Should be used only on TW machine
 underlay:
 	rm -rf $(share)/underlay
-	MoinMoin/script/moin.py --config-dir=/srv/de.wikiwikiweb.moinmaster/bin15 --wiki-url=moinmaster.wikiwikiweb.de/ maint globaledit
-	MoinMoin/script/moin.py --config-dir=/srv/de.wikiwikiweb.moinmaster/bin15 --wiki-url=moinmaster.wikiwikiweb.de/ maint reducewiki --target-dir=$(share)/underlay
+	MoinMoin/script/moin.py --config-dir=/srv/de.wikiwikiweb.moinmaster/bin16 --wiki-url=master.moinmo.in/ maint globaledit
+	MoinMoin/script/moin.py --config-dir=/srv/de.wikiwikiweb.moinmaster/bin16 --wiki-url=master.moinmo.in/ maint reducewiki --target-dir=$(share)/underlay
 	rm -rf $(share)/underlay/pages/InterWikiMap/
 	echo -ne "#acl All:read\r\nSee MoinMoin:EditingOnMoinMaster.\r\n" > \
 	    $(share)/underlay/pages/MoinPagesEditorGroup/revisions/00000001
--- a/MoinMoin/Page.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/Page.py	Sun Jan 06 23:39:24 2008 +0100
@@ -978,7 +978,7 @@
             request.setHttpHeader("Status: 200 OK")
             request.setHttpHeader("Last-Modified: %s" % util.timefuncs.formathttpdate(os.path.getmtime(self._text_filename())))
             text = self.encodeTextMimeType(self.body)
-            request.setHttpHeader("Content-Length: %d" % len(text))
+            #request.setHttpHeader("Content-Length: %d" % len(text))  # XXX WRONG! text is unicode obj, but we send utf-8!
             if content_disposition:
                 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
                 # There is no solution that is compatible to IE except stripping non-ascii chars
@@ -1094,6 +1094,10 @@
                     request.setHttpHeader('Status: 404 NOTFOUND')
                 request.emit_http_headers()
 
+            if not page_exists and self.request.isSpiderAgent:
+                # don't send any 404 content to bots
+                return
+
             request.write(self.formatter.startDocument(self.page_name))
 
             # send the page header
--- a/MoinMoin/PageEditor.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/PageEditor.py	Sun Jan 06 23:39:24 2008 +0100
@@ -399,6 +399,9 @@
 <input type="hidden" name="editor" value="text">
 ''' % (button_spellcheck, cancel_button_text, ))
 
+        from MoinMoin.security.textcha import TextCha
+        request.write(TextCha(request).render())
+
         # Add textarea with page text
         self.sendconfirmleaving()
 
--- a/MoinMoin/PageGraphicalEditor.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/PageGraphicalEditor.py	Sun Jan 06 23:39:24 2008 +0100
@@ -288,6 +288,9 @@
 <input type="hidden" name="editor" value="gui">
 ''' % (button_spellcheck, cancel_button_text, ))
 
+        from MoinMoin.security.textcha import TextCha
+        request.write(TextCha(request).render())
+
         self.sendconfirmleaving() # TODO update state of flgChange to make this work, see PageEditor
 
         # Add textarea with page text
--- a/MoinMoin/action/AttachFile.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/action/AttachFile.py	Sun Jan 06 23:39:24 2008 +0100
@@ -31,6 +31,7 @@
 from MoinMoin import config, wikiutil, packages
 from MoinMoin.Page import Page
 from MoinMoin.util import filesys, timefuncs
+from MoinMoin.security.textcha import TextCha
 from MoinMoin.events import FileAttachedEvent, send_event
 import MoinMoin.events.notification as notification
 
@@ -514,6 +515,7 @@
 <dt>%(upload_label_overwrite)s</dt>
 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
 </dl>
+%(textcha)s
 <p>
 <input type="hidden" name="action" value="%(action_name)s">
 <input type="hidden" name="do" value="upload">
@@ -530,6 +532,7 @@
     'upload_label_overwrite': _('Overwrite existing attachment of same name'),
     'overwrite_checked': ('', 'checked')[request.form.get('overwrite', ['0'])[0] == '1'],
     'upload_button': _('Upload'),
+    'textcha': TextCha(request).render(),
 })
 
 #<dt>%(upload_label_mime)s</dt>
@@ -555,14 +558,59 @@
     """
     _ = request.getText
 
-    msg = None
-    do = request.form.get('do')
-    if do is not None:
-        do = do[0]
     if action_name in request.cfg.actions_excluded:
         msg = _('File attachments are not allowed in this wiki!')
-    elif 'do' not in request.form:
+        error_msg(pagename, request, msg)
+        return
+
+    do = request.form.get('do')
+    if do is None:
         upload_form(pagename, request)
+        return
+
+    msg = None
+    do = do[0]
+
+    # First handle read-only access to attachments:
+    if do == 'get':
+        if request.user.may.read(pagename):
+            get_file(pagename, request)
+        else:
+            msg = _('You are not allowed to get attachments from this page.')
+    elif do == 'view':
+        if request.user.may.read(pagename):
+            view_file(pagename, request)
+        else:
+            msg = _('You are not allowed to view attachments of this page.')
+    elif do == 'move':
+        if request.user.may.delete(pagename):
+            send_moveform(pagename, request)
+        else:
+            msg = _('You are not allowed to move attachments from this page.')
+
+    # Second handle write access:
+    elif do == 'upload':
+        # Currently we only check TextCha for upload (this is what spammers ususally do),
+        # but it could be extended to more/all attachment write access
+        if not TextCha(request).check_answer_from_form():
+            msg = _('TextCha: Wrong answer! Go back and try again...', formatted=False)
+        else:
+            overwrite = 0
+            if 'overwrite' in request.form:
+                try:
+                    overwrite = int(request.form['overwrite'][0])
+                except:
+                    pass
+            if (not overwrite and request.user.may.write(pagename)) or \
+               (overwrite and request.user.may.write(pagename) and request.user.may.delete(pagename)):
+                if 'file' in request.form:
+                    do_upload(pagename, request, overwrite)
+                else:
+                    # This might happen when trying to upload file names
+                    # with non-ascii characters on Safari.
+                    msg = _("No file content. Delete non ASCII characters from the file name and try again.")
+            else:
+                msg = _('You are not allowed to attach a file to this page.')
     elif do == 'savedrawing':
         if request.user.may.write(pagename):
             save_drawing(pagename, request)
@@ -570,33 +618,11 @@
             request.write("OK")
         else:
             msg = _('You are not allowed to save a drawing on this page.')
-    elif do == 'upload':
-        overwrite = 0
-        if 'overwrite' in request.form:
-            try:
-                overwrite = int(request.form['overwrite'][0])
-            except:
-                pass
-        if (not overwrite and request.user.may.write(pagename)) or \
-           (overwrite and request.user.may.write(pagename) and request.user.may.delete(pagename)):
-            if 'file' in request.form:
-                do_upload(pagename, request, overwrite)
-            else:
-                # This might happen when trying to upload file names
-                # with non-ascii characters on Safari.
-                msg = _("No file content. Delete non ASCII characters from the file name and try again.")
-        else:
-            msg = _('You are not allowed to attach a file to this page.')
     elif do == 'del':
         if request.user.may.delete(pagename):
             del_file(pagename, request)
         else:
             msg = _('You are not allowed to delete attachments on this page.')
-    elif do == 'move':
-        if request.user.may.delete(pagename):
-            send_moveform(pagename, request)
-        else:
-            msg = _('You are not allowed to move attachments from this page.')
     elif do == 'attachment_move':
         if 'cancel' in request.form:
             msg = _('Move aborted!')
@@ -610,11 +636,6 @@
             attachment_move(pagename, request)
         else:
             msg = _('You are not allowed to move attachments from this page.')
-    elif do == 'get':
-        if request.user.may.read(pagename):
-            get_file(pagename, request)
-        else:
-            msg = _('You are not allowed to get attachments from this page.')
     elif do == 'unzip':
         if request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename):
             unzip_file(pagename, request)
@@ -625,13 +646,8 @@
             install_package(pagename, request)
         else:
             msg = _('You are not allowed to install files.')
-    elif do == 'view':
-        if request.user.may.read(pagename):
-            view_file(pagename, request)
-        else:
-            msg = _('You are not allowed to view attachments of this page.')
     else:
-        msg = _('Unsupported upload action: %s') % (wikiutil.escape(do), )
+        msg = _('Unsupported AttachFile sub-action: %s') % (wikiutil.escape(do), )
 
     if msg:
         error_msg(pagename, request, msg)
--- a/MoinMoin/action/LikePages.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/action/LikePages.py	Sun Jan 06 23:39:24 2008 +0100
@@ -232,9 +232,9 @@
         for key in keys:
             if matches[key] == match:
                 request.write(request.formatter.listitem(1))
-                request.write(request.formatter.pagelink(1, key))
+                request.write(request.formatter.pagelink(1, key, generated=True))
                 request.write(request.formatter.text(key))
-                request.write(request.formatter.pagelink(0, key))
+                request.write(request.formatter.pagelink(0, key, generated=True))
                 request.write(request.formatter.listitem(0))
         request.write(request.formatter.bullet_list(0))
 
--- a/MoinMoin/action/diff.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/action/diff.py	Sun Jan 06 23:39:24 2008 +0100
@@ -111,7 +111,7 @@
         from MoinMoin.util import diff_text
         lines = diff_text.diff(oldpage.getlines(), newpage.getlines())
         if not lines:
-            msg = f.text(_("No differences found!"))
+            msg = f.text(" - " + _("No differences found!", formatted=False))
             if edit_count > 1:
                 msg = msg + f.paragraph(1) + f.text(_('The page was saved %(count)d times, though!') % {
                     'count': edit_count}) + f.paragraph(0)
--- a/MoinMoin/action/edit.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/action/edit.py	Sun Jan 06 23:39:24 2008 +0100
@@ -158,6 +158,9 @@
     # Save new text
     else:
         try:
+            from MoinMoin.security.textcha import TextCha
+            if not TextCha(request).check_answer_from_form():
+                raise pg.SaveError(_('TextCha: Wrong answer! Go back and try again...', formatted=False))
             savemsg = pg.saveText(savetext, rev, trivial=trivial, comment=comment)
         except pg.EditConflict, e:
             msg = e.message
--- a/MoinMoin/action/fullsearch.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/action/fullsearch.py	Sun Jan 06 23:39:24 2008 +0100
@@ -198,7 +198,12 @@
         page = results.hits[0]
         if not page.attachment: # we did not find an attachment
             page = Page(request, page.page_name)
-            url = page.url(request, querystr={'highlight': query.highlight_re()}, relative=False)
+            highlight = query.highlight_re()
+            if highlight:
+                querydict = {'highlight': highlight}
+            else:
+                querydict = {}
+            url = page.url(request, querystr=querydict, relative=False)
             request.http_redirect(url)
             return
     elif not results.hits: # no hits?
--- a/MoinMoin/action/newaccount.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/action/newaccount.py	Sun Jan 06 23:39:24 2008 +0100
@@ -10,6 +10,7 @@
 from MoinMoin.Page import Page
 from MoinMoin.widget import html
 import MoinMoin.events as events
+from MoinMoin.security.textcha import TextCha
 
 
 _debug = False
@@ -20,6 +21,10 @@
 
     if request.request_method != 'POST':
         return _("Use UserPreferences to change your settings or create an account.")
+           
+    if not TextCha(request).check_answer_from_form():
+        return _('TextCha: Wrong answer! Go back and try again...', formatted=False)
+
     # Create user profile
     theuser = user.User(request, auth_method="new-user")
 
@@ -43,18 +48,18 @@
     password = form.get('password', [''])[0]
     password2 = form.get('password2', [''])[0]
 
+    # Check if password is given and matches with password repeat
+    if password != password2:
+        return _("Passwords don't match!")
+    if not password:
+        return _("Please specify a password!")
+
     pw_checker = request.cfg.password_checker
     if pw_checker:
         pw_error = pw_checker(theuser.name, password)
         if pw_error:
             return _("Password not acceptable: %s") % pw_error
 
-    # Check if password is given and matches with password repeat
-    if password != password2:
-        return _("Passwords don't match!")
-    if not password:
-        return _("Please specify a password!")
-
     # Encode password
     if password and not password.startswith('{SHA}'):
         try:
@@ -127,6 +132,16 @@
 
     row = html.TR()
     tbl.append(row)
+    row.append(html.TD().append(html.STRONG().append(
+                                  html.Text(_('TextCha (required)', formatted=False))))) 
+    td = html.TD()
+    textcha = TextCha(request).render()
+    if textcha:
+        td.append(textcha)
+    row.append(td)
+
+    row = html.TR()
+    tbl.append(row)
     row.append(html.TD())
     td = html.TD()
     row.append(td)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/userprofile.py	Sun Jan 06 23:39:24 2008 +0100
@@ -0,0 +1,33 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - set values in user profile
+
+    @copyright: 2008 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.Page import Page
+from MoinMoin import user
+ 
+def execute(pagename, request):
+    """ set values in user profile """
+    _ = request.getText
+    cfg = request.cfg
+    form = request.form
+
+    if not request.user.isSuperUser():
+        request.theme.add_msg(_("Only superuser is allowed to use this action."), "error")
+    else:
+       user_name = form.get('name', [''])[0]
+       key = form.get('key', [''])[0]
+       val = form.get('val', [''])[0]
+       if key in cfg.user_checkbox_fields:
+           val = int(val)
+       uid = user.getUserId(request, user_name)
+       theuser = user.User(request, uid)
+       oldval = getattr(theuser, key)
+       setattr(theuser, key, val)
+       theuser.save()
+       request.theme.add_msg('%s.%s: %s -> %s' % (user_name, key, oldval, val), "info")
+
+    Page(request, pagename).send_page()
+
--- a/MoinMoin/auth/ldap_login.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/auth/ldap_login.py	Sun Jan 06 23:39:24 2008 +0100
@@ -5,11 +5,18 @@
     This code only creates a user object, the session has to be established by
     the auth.moin_session auth plugin.
 
+    python-ldap needs to be at least 2.0.0pre06 (available since mid 2002) for
+    ldaps support - some older debian installations (woody and older?) require
+    libldap2-tls and python2.x-ldap-tls, otherwise you get ldap.SERVER_DOWN:
+    "Can't contact LDAP server" - more recent debian installations have tls
+    support in libldap2 (see dependency on gnutls) and also in python-ldap.
+
     TODO: migrate configuration items to constructor parameters,
           allow more configuration (alias name, ...) by using
           callables as parameters
 
-    @copyright: 2006 MoinMoin:ThomasWaldmann, Nick Phillips
+    @copyright: 2006-2007 MoinMoin:ThomasWaldmann,
+                2006 Nick Phillips
     @license: GNU GPL, see COPYING for details.
 """
 import sys
@@ -18,11 +25,11 @@
 from MoinMoin import user
 from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin
 
+
 class LDAPAuth(BaseAuth):
-    """ 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.
+    """ get authentication data from form, authenticate against LDAP (or Active
+        Directory), fetch some user infos from LDAP and create a user object
+        for that user. The session is kept by the moin_session auth plugin.
     """
 
     login_inputs = ['username', 'password']
@@ -48,23 +55,38 @@
                 dn = None
                 coding = cfg.ldap_coding
                 if verbose: request.log("LDAP: Setting misc. options...")
-                # needed for Active Directory:
-                ldap.set_option(ldap.OPT_REFERRALS, 0)
+                ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+                ldap.set_option(ldap.OPT_REFERRALS, cfg.ldap_referrals)
+                ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, cfg.ldap_timeout)
+
+                starttls = cfg.ldap_start_tls
+                if ldap.TLS_AVAIL:
+                    for option, value in (
+                        (ldap.OPT_X_TLS_CACERTDIR, cfg.ldap_tls_cacertdir),
+                        (ldap.OPT_X_TLS_CACERTFILE, cfg.ldap_tls_cacertfile),
+                        (ldap.OPT_X_TLS_CERTFILE, cfg.ldap_tls_certfile),
+                        (ldap.OPT_X_TLS_KEYFILE, cfg.ldap_tls_keyfile),
+                        (ldap.OPT_X_TLS_REQUIRE_CERT, cfg.ldap_tls_require_cert),
+                        (ldap.OPT_X_TLS, starttls),
+                        #(ldap.OPT_X_TLS_ALLOW, 1),
+                    ):
+                        if value:
+                            ldap.set_option(option, value)
 
                 server = cfg.ldap_uri
-                if server.startswith('ldaps:'):
-                    # TODO: refactor into LDAPAuth() constructor arguments!
-                    # this is needed for self-signed ssl certs:
-                    ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
-                    # more stuff to try:
-                    #ldap.set_option(ldap.OPT_X_TLS_ALLOW, 1)
-                    #ldap.set_option(ldap.OPT_X_TLS_CERTFILE, LDAP_CACERTFILE)
-                    #ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,'/etc/httpd/ssl.crt/myCA-cacerts.pem')
-
                 if verbose: request.log("LDAP: Trying to initialize %r." % server)
                 l = ldap.initialize(server)
                 if verbose: request.log("LDAP: Connected to LDAP server %r." % server)
 
+                if starttls and server.startswith('ldap:'):
+                    if verbose: request.log("LDAP: Trying to start TLS to %r." % server)
+                    try:
+                        l.start_tls_s()
+                        if verbose: request.log("LDAP: Using TLS to %r." % server)
+                    except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR), err:
+                        if verbose: request.log("LDAP: Couldn't establish TLS to %r (err: %s)." % (server, str(err)))
+                        raise
+  
                 # 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()
@@ -74,12 +96,14 @@
                 # you can use %(username)s here to get the stuff entered in the form:
                 filterstr = cfg.ldap_filter % locals()
                 if verbose: request.log("LDAP: Searching %r" % filterstr)
+                attrs = [getattr(cfg, attr) for attr in [
+                                         'ldap_email_attribute',
+                                         'ldap_aliasname_attribute',
+                                         'ldap_surname_attribute',
+                                         'ldap_givenname_attribute',
+                                         ] if getattr(cfg, attr) is not None]
                 lusers = l.search_st(cfg.ldap_base, cfg.ldap_scope, filterstr.encode(coding),
-                                     attrlist=[cfg.ldap_email_attribute,
-                                               cfg.ldap_aliasname_attribute,
-                                               cfg.ldap_surname_attribute,
-                                               cfg.ldap_givenname_attribute,
-                                     ], timeout=cfg.ldap_timeout)
+                                     attrlist=attrs, timeout=cfg.ldap_timeout)
                 # we remove entries with dn == None to get the real result list:
                 lusers = [(dn, ldap_dict) for dn, ldap_dict in lusers if dn is not None]
                 if verbose:
@@ -101,8 +125,11 @@
                 l.simple_bind_s(dn, password.encode(coding))
                 if verbose: request.log("LDAP: Bound with dn %r (username: %r)" % (dn, username))
 
-                if getattr(cfg, "ldap_email_callback", None) is None:
-                    email = ldap_dict.get(cfg.ldap_email_attribute, [''])[0].decode(coding)
+                if cfg.ldap_email_callback is None:
+                    if cfg.ldap_email_attribute:
+                        email = ldap_dict.get(cfg.ldap_email_attribute, [''])[0].decode(coding)
+                    else:
+                        email = None
                 else:
                     email = cfg.ldap_email_callback(ldap_dict)
 
@@ -110,6 +137,8 @@
                 try:
                     aliasname = ldap_dict[cfg.ldap_aliasname_attribute][0]
                 except (KeyError, IndexError):
+                    pass
+                if not aliasname:
                     sn = ldap_dict.get(cfg.ldap_surname_attribute, [''])[0]
                     gn = ldap_dict.get(cfg.ldap_givenname_attribute, [''])[0]
                     if sn and gn:
@@ -118,10 +147,13 @@
                         aliasname = sn
                 aliasname = aliasname.decode(coding)
 
-                u = user.User(request, auth_username=username, password="{SHA}NotStored", auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author', ))
+                if email:
+                    u = user.User(request, auth_username=username, password="{SHA}NotStored", auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author', ))
+                    u.email = email
+                else:
+                    u = user.User(request, auth_username=username, password="{SHA}NotStored", auth_method=self.name, auth_attribs=('name', 'password', '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 %r email %r alias %r" % (username, email, aliasname))
 
--- a/MoinMoin/config/multiconfig.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/config/multiconfig.py	Sun Jan 06 23:39:24 2008 +0100
@@ -11,6 +11,7 @@
 import os
 import sys
 import time
+import logging
 
 from MoinMoin import config, error, util, wikiutil
 import MoinMoin.auth as authmodule
@@ -329,16 +330,65 @@
     language_ignore_browser = False # ignore browser settings, use language_default
                                     # or user prefs
 
+    # ldap / active directory server URI
+    # use ldaps://server:636 url for ldaps,
+    # use  ldap://server for ldap without tls (and set ldap_start_tls to 0),
+    # use  ldap://server for ldap with tls (and set ldap_start_tls to 1 or 2).
+    ldap_uri = 'ldap://localhost'
+
+    # We can either use some fixed user and password for binding to LDAP.
+    # Be careful if you need a % char in those strings - as they are used as
+    # a format string, you have to write %% to get a single % in the end.
+    #ldap_binddn = 'binduser@example.org' # (AD)
+    #ldap_binddn = 'cn=admin,dc=example,dc=org' # (OpenLDAP)
+    #ldap_bindpw = 'secret'
+    # or we can use the username and password we got from the user:
+    #ldap_binddn = '%(username)s@example.org' # DN we use for first bind (AD)
+    #ldap_bindpw = '%(password)s' # password we use for first bind
+    # or we can bind anonymously (if that is supported by your directory).
+    # In any case, ldap_binddn and ldap_bindpw must be defined.
+    ldap_binddn = ''
+    ldap_bindpw = ''
+
+    # base DN we use for searching
+    #ldap_base = 'ou=SOMEUNIT,dc=example,dc=org'
+    ldap_base = ''
+
+    # scope of the search we do (2 == ldap.SCOPE_SUBTREE)
+    ldap_scope = 2 # we do not want to import ldap for everybody just for that
+
+    # LDAP REFERRALS
+    ldap_referrals = 0 # (0 needed for AD)
+
+    # ldap filter used for searching:
+    #ldap_filter = '(sAMAccountName=%(username)s)' # (AD)
+    ldap_filter = '(uid=%(username)s)' # (OpenLDAP)
+    # you can also do more complex filtering like:
+    # "(&(cn=%(username)s)(memberOf=CN=WikiUsers,OU=Groups,DC=example,DC=org))"
+
+    # some attribute names we use to extract information from LDAP:
+    ldap_givenname_attribute = None # ('givenName') ldap attribute we get the first name from
+    ldap_surname_attribute = None # ('sn') ldap attribute we get the family name from
+    ldap_aliasname_attribute = None # ('displayName') ldap attribute we get the aliasname from
+    ldap_email_attribute = None # ('mail') ldap attribute we get the email address from
+    ldap_email_callback = None # called to make up email address
+
+    ldap_coding = 'utf-8' # coding used for ldap queries and result values
+    ldap_timeout = 10 # how long we wait for the ldap server [s]
+    ldap_verbose = True # if True, put lots of LDAP debug info into the log
+
+    # TLS / SSL related defaults
+    ldap_start_tls = 0 # 0 = No, 1 = Try, 2 = Required
+    ldap_tls_cacertdir = ''
+    ldap_tls_cacertfile = ''
+    ldap_tls_certfile = ''
+    ldap_tls_keyfile = ''
+    ldap_tls_require_cert = 0 # 0 == ldap.OPT_X_TLS_NEVER (needed for self-signed certs)
+
     log_reverse_dns_lookups = True  # if we do reverse dns lookups for logging hostnames
                                     # instead of just IPs
     log_timing = False              # update <data_dir>/timing.log?
 
-    xapian_search = False
-    xapian_index_dir = None
-    xapian_stemming = True
-    xapian_index_history = False
-    search_results_per_page = 10
-
     mail_login = None # or "user pwd" if you need to use SMTP AUTH
     mail_sendmail = None # "/usr/sbin/sendmail -t -i" to not use SMTP, but sendmail
     mail_smarthost = None
@@ -531,6 +581,9 @@
     }
     surge_lockout_time = 3600 # secs you get locked out when you ignore warnings
 
+    textchas = None
+    textchas_disabled_group = None # e.g. u'NoTextChasGroup' if you are a member of this group, you don't get textchas
+
     theme_default = 'modern'
     theme_force = False
 
@@ -546,9 +599,10 @@
 
     # a regex of HTTP_USER_AGENTS that should be excluded from logging
     # and receive a FORBIDDEN for anything except viewing a page
-    ua_spiders = ('archiver|cfetch|crawler|curl|gigabot|googlebot|holmes|htdig|httrack|httpunit|jeeves|larbin|leech|'
-                  'linkbot|linkmap|linkwalk|mercator|mirror|msnbot|msrbot|neomo|nutbot|omniexplorer|puf|robot|scooter|seekbot|'
-                  'sherlock|slurp|sitecheck|spider|teleport|voyager|webreaper|wget')
+    ua_spiders = ('archiver|cfetch|charlotte|crawler|curl|gigabot|googlebot|heritrix|holmes|htdig|httrack|httpunit|'
+                  'intelix|java|jeeves|larbin|leech|libwww-perl|linkbot|linkmap|linkwalk|litefinder|mercator|'
+                  'microsoft.url.control|mirror| mj12bot|msnbot|msrbot|neomo|nutbot|omniexplorer|puf|robot|scooter|seekbot|'
+                  'sherlock|slurp|sitecheck|snoopy|spider|teleport|twiceler|voilabot|voyager|webreaper|wget|yeti')
 
     # Wiki identity
     sitename = u'Untitled Wiki'
@@ -659,6 +713,12 @@
     unzip_attachments_space = 200.0 * 1000 ** 2
     unzip_attachments_count = 101 # 1 zip file + 100 files contained in it
 
+    xapian_search = False
+    xapian_index_dir = None
+    xapian_stemming = True
+    xapian_index_history = False
+    search_results_per_page = 10
+
     SecurityPolicy = None
 
     def __init__(self, siteid):
@@ -734,6 +794,14 @@
         self.navi_bar = [elem % self for elem in self.navi_bar]
         self.backup_exclude = [elem % self for elem in self.backup_exclude]
 
+        # check if python-xapian is installed
+        if self.xapian_search:
+            try:
+                import xapian
+            except ImportError, err:
+                self.xapian_search = False
+                logging.error("xapian_search was auto-disabled because python-xapian is not installed [%s]." % str(err))
+
         # list to cache xapian searcher objects
         self.xapian_searchers = []
 
--- a/MoinMoin/formatter/text_docbook.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/formatter/text_docbook.py	Sun Jan 06 23:39:24 2008 +0100
@@ -396,12 +396,12 @@
 ### Attachments ######################################################
 
     def attachment_link(self, on, url=None, **kw):
-        _ = self.request.getText
-        pagename, filename = AttachFile.absoluteName(url, self.page.page_name)
-        fname = wikiutil.taintfilename(filename)
-        target = AttachFile.getAttachUrl(pagename, filename, self.request)
+        assert on in (0, 1, False, True) # make sure we get called the new way, not like the 1.5 api was
         # we do not output a "upload link" when outputting docbook
         if on:
+            pagename, filename = AttachFile.absoluteName(url, self.page.page_name)
+            fname = wikiutil.taintfilename(filename)
+            target = AttachFile.getAttachUrl(pagename, filename, self.request)
             return self.url(1, target, title="attachment:%s" % url)
         else:
             return self.url(0)
--- a/MoinMoin/i18n/Makefile	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/i18n/Makefile	Sun Jan 06 23:39:24 2008 +0100
@@ -14,7 +14,6 @@
 	@lang=`echo $@ | sed -e 's/\.MoinMoin\.po-update$$//'`; \
 	echo "$$lang:"; \
 	tools/wiki2po.py $${lang}; \
-	tools/markup15to16.py $${lang}; \
 	echo "msgmerge $$lang.$(DOMAIN).po $(DOMAIN).pot -o $$lang.$(DOMAIN).new.po"; \
 	if msgmerge $$lang.$(DOMAIN).po $(DOMAIN).pot -o $$lang.$(DOMAIN).new.po; then \
 	  if cmp $$lang.$(DOMAIN).po $$lang.$(DOMAIN).new.po >/dev/null 2>&1; then \
--- a/MoinMoin/i18n/msgfmt.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/i18n/msgfmt.py	Sun Jan 06 23:39:24 2008 +0100
@@ -23,13 +23,13 @@
         Display version information and exit.
 
 Written by Martin v. L÷wis <loewis@informatik.hu-berlin.de>,
-refactored by Thomas Waldmann <tw AT waldmann-edv DOT de>.
+refactored / fixed by Thomas Waldmann <tw AT waldmann-edv DOT de>.
 """
 
 import sys, os
 import getopt, struct, array
 
-__version__ = "1.2"
+__version__ = "1.3"
 
 class SyntaxErrorException(Exception):
     """raised when having trouble parsing the po file content"""
@@ -80,6 +80,7 @@
             if line.startswith('msgid'):
                 if section == STR:
                     self.add(msgid, msgstr, fuzzy)
+                    fuzzy = False
                 section = ID
                 line = line[5:]
                 msgid = msgstr = ''
--- a/MoinMoin/macro/MonthCalendar.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/macro/MonthCalendar.py	Sun Jan 06 23:39:24 2008 +0100
@@ -248,6 +248,13 @@
     else:
         year, month = yearmonthplusoffset(parmyear, parmmonth, parmoffset)
 
+    if request.isSpiderAgent and abs(currentyear - year) > 1:
+        return '' # this is a bot and it didn't follow the rules (see below)
+    if currentyear == year:
+        attrs = {}
+    else:
+        attrs = {'rel': 'nofollow' } # otherwise even well-behaved bots will index forever
+
     # get the calendar
     monthcal = calendar.monthcalendar(year, month)
 
@@ -270,10 +277,11 @@
     nextlink = p.url(request, querystr % (qpagenames, parmoffset2 + 1, qtemplate), relative=False)
     prevylink = p.url(request, querystr % (qpagenames, parmoffset2 - 12, qtemplate), relative=False)
     nextylink = p.url(request, querystr % (qpagenames, parmoffset2 + 12, qtemplate), relative=False)
-    prevmonth = formatter.url(1, prevlink, 'cal-link') + '&lt;' + formatter.url(0)
-    nextmonth = formatter.url(1, nextlink, 'cal-link') + '&gt;' + formatter.url(0)
-    prevyear = formatter.url(1, prevylink, 'cal-link') + '&lt;&lt;' + formatter.url(0)
-    nextyear = formatter.url(1, nextylink, 'cal-link') + '&gt;&gt;' + formatter.url(0)
+
+    prevmonth = formatter.url(1, prevlink, 'cal-link', **attrs) + '&lt;' + formatter.url(0)
+    nextmonth = formatter.url(1, nextlink, 'cal-link', **attrs) + '&gt;' + formatter.url(0)
+    prevyear = formatter.url(1, prevylink, 'cal-link', **attrs) + '&lt;&lt;' + formatter.url(0)
+    nextyear = formatter.url(1, nextylink, 'cal-link', **attrs) + '&gt;&gt;' + formatter.url(0)
 
     if parmpagename != [thispage]:
         pagelinks = ''
@@ -289,6 +297,7 @@
             ch = parmpagename[0][st:st+chstep]
             r, g, b = cliprgb(r, g, b)
             link = Page(request, parmpagename[0]).link_to(request, ch,
+                        rel='nofollow',
                         style='background-color:#%02x%02x%02x;color:#000000;text-decoration:none' % (r, g, b))
             pagelinks = pagelinks + link
             r, g, b = (r, g+colorstep, b)
@@ -296,6 +305,7 @@
         r, g, b = (255-colorstep, 255, 255-colorstep)
         for page in parmpagename[1:]:
             link = Page(request, page).link_to(request, page,
+                        rel='nofollow',
                         style='background-color:#%02x%02x%02x;color:#000000;text-decoration:none' % (r, g, b))
             pagelinks = pagelinks + '*' + link
         showpagename = '   %s<BR>\n' % pagelinks
@@ -359,8 +369,8 @@
                     tiptitle = link
                     tiptext = '<br>'.join(titletext)
                     maketip_js.append("maketip('%s','%s','%s');" % (tipname, tiptitle, tiptext))
-                    onmouse = {'onMouseOver': "tip('%s')" % tipname,
-                               'onMouseOut': "untip()"}
+                    attrs = {'onMouseOver': "tip('%s')" % tipname,
+                             'onMouseOut': "untip()"}
                 else:
                     csslink = "cal-emptyday"
                     if parmtemplate:
@@ -370,7 +380,7 @@
                     r, g, b, u = (255, 255, 255, 0)
                     if wkday in wkend:
                         csslink = "cal-weekend"
-                    onmouse = {}
+                    attrs = {'rel': 'nofollow'}
                 for otherpage in parmpagename[1:]:
                     otherlink = "%s/%4d-%02d-%02d" % (otherpage, year, month, day)
                     otherdaypage = Page(request, otherlink)
@@ -382,7 +392,7 @@
                             r, g, b = (r, g+colorstep, b)
                 r, g, b = cliprgb(r, g, b)
                 style = 'background-color:#%02x%02x%02x' % (r, g, b)
-                fmtlink = formatter.url(1, daypage.url(request, query, relative=False), csslink, **onmouse) + str(day) + formatter.url(0)
+                fmtlink = formatter.url(1, daypage.url(request, query, relative=False), csslink, **attrs) + str(day) + formatter.url(0)
                 if day == currentday and month == currentmonth and year == currentyear:
                     cssday = "cal-today"
                     fmtlink = "<b>%s</b>" % fmtlink # for browser with CSS probs
--- a/MoinMoin/request/__init__.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/request/__init__.py	Sun Jan 06 23:39:24 2008 +0100
@@ -3,12 +3,45 @@
     MoinMoin - RequestBase Implementation
 
     @copyright: 2001-2003 Juergen Hermann <jh@web.de>,
-                2003-2006 MoinMoin:ThomasWaldmann
+                2003-2008 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
+# Support for remote IP address detection when using (reverse) proxy (or even proxies).
+# If you exactly KNOW which (reverse) proxies you can trust, put them into the list
+# below, so we can determine the "outside" IP as your trusted proxies see it.
+
+proxies_trusted = [] # trust noone!
+#proxies_trusted = ['127.0.0.1', ] # can be a list of multiple IPs
+
+import logging
+proxy_loglevel = logging.DEBUG # logging.NOTSET (never), logging.INFO (when not debugging)
+
+def find_remote_addr(addrs):
+    """ Find the last remote IP address before it hits our reverse proxies.
+        The LAST address in the <addrs> list is the remote IP as detected by the server
+        (not taken from some x-forwarded-for header).
+        The FIRST address in the <addrs> list might be the client's IP - if noone cheats
+        and everyone supports x-f-f header.
+
+        See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/                                                          
+
+        For debug loglevel, we log all <addrs>.
+
+        TODO: refactor request code to first do some basic IP init, then load configuration,
+        TODO: then do proxy processing.
+        TODO: add wikiconfig configurability for proxies_trusted
+        TODO: later, make it possible to put multipe remote IP addrs into edit-log
+    """
+    logging.log(proxy_loglevel, "request.find_remote_addr: addrs == %r" % addrs)
+    if proxies_trusted:
+        result = [addr for addr in addrs if addr not in proxies_trusted]
+        if result:
+            return result[-1] # last IP before it hit our trusted (reverse) proxies
+    return addrs[-1] # this is a safe remote_addr, not taken from x-f-f header
+
+
 import os, re, time, sys, cgi, StringIO
-import logging
 import Cookie
 import traceback
 
@@ -72,7 +105,8 @@
     # Extra headers we support. Both standalone and twisted store
     # headers as lowercase.
     moin_location = 'x-moin-location'
-    proxy_host = 'x-forwarded-host'
+    proxy_host = 'x-forwarded-host' # original host: header as seen by the proxy (e.g. wiki.example.org)
+    proxy_xff = 'x-forwarded-for' # list of original remote_addrs as seen by the proxies (e.g. <clientip>,<proxy1>,<proxy2>,...)
 
     def __init__(self, properties={}):
 
@@ -375,6 +409,7 @@
         self.setIsSSL(env)
         self.setHost(env.get('HTTP_HOST'))
         self.fixURI(env)
+
         self.setURL(env)
         #self.debugEnvironment(env)
 
@@ -460,10 +495,10 @@
 
         @param env: dict like object containing cgi meta variables or http headers.
         """
-        # If we serve on localhost:8000 and use a proxy on
-        # example.com/wiki, our urls will be example.com/wiki/pagename
-        # Same for the wiki config - they must use the proxy url.
+        # proxy support
+        self.rewriteRemoteAddr(env)
         self.rewriteHost(env)
+
         self.rewriteURI(env)
 
         if not self.request_uri:
@@ -489,6 +524,27 @@
         if proxy_host:
             self.http_host = proxy_host
 
+    def rewriteRemoteAddr(self, env):
+        """ Rewrite remote_addr transparently
+        
+        Get the proxy remote addr using 'X-Forwarded-For' header, added by
+        Apache 2 and other proxy software.
+        
+        TODO: Will not work for Apache 1 or others that don't add this header.
+        
+        TODO: If we want to add an option to disable this feature it
+        should be in the server script, because the config is not
+        loaded at this point, and must be loaded after url is set.
+        
+        @param env: dict like object containing cgi meta variables or http headers.
+        """
+        xff = (env.get(self.proxy_xff) or
+               env.get(cgiMetaVariable(self.proxy_xff)))
+        if xff:
+            xff = [addr.strip() for addr in xff.split(',')]
+            xff.append(self.remote_addr)
+            self.remote_addr = find_remote_addr(xff)
+
     def rewriteURI(self, env):
         """ Rewrite request_uri, script_name and path_info transparently
 
@@ -805,15 +861,13 @@
                 indicator += '!1!'
             total = self.clock.value('total')
             # use + for existing pages, - for non-existing pages
-            indicator += self.page.exists() and '+' or '-'
+            if self.page is not None:
+                indicator += self.page.exists() and '+' or '-'
             if self.isSpiderAgent:
                 indicator += "B"
 
-        # Add time stamp and process ID
         pid = os.getpid()
-        t = time.time()
-        timestr = time.strftime("%Y%m%d %H%M%S", time.gmtime(t))
-        msg = '%s %5d %-6s %4s %-10s %s\n' % (timestr, pid, total, indicator, action, self.url)
+        msg = 'Timing %5d %-6s %4s %-10s %s\n' % (pid, total, indicator, action, self.url)
         self.log(msg)
 
     def write(self, *data):
@@ -1106,19 +1160,21 @@
         self.initTheme()
 
         action_name = self.action
+        if self.cfg.log_timing:
+            self.timing_log(True, action_name)
+
         if action_name == 'xmlrpc':
             from MoinMoin import xmlrpc
             if self.query_string == 'action=xmlrpc':
                 xmlrpc.xmlrpc(self)
             elif self.query_string == 'action=xmlrpc2':
                 xmlrpc.xmlrpc2(self)
+            if self.cfg.log_timing:
+                self.timing_log(False, action_name)
             return self.finish()
 
         # parse request data
         try:
-            if self.cfg.log_timing:
-                self.timing_log(True, action)
-
             # The last component in path_info is the page name, if any
             path = self.getPathinfo()
 
@@ -1130,7 +1186,7 @@
                 if path.startswith(prefix):
                     # remove prefix and action name
                     path = path[len(prefix):]
-                    action, path = path.split('/', 1)
+                    action, path = (path.split('/', 1) + ['', ''])[:2]
                     path = '/' + path
 
             if path.startswith('/'):
@@ -1214,12 +1270,14 @@
 
         except MoinMoinFinish:
             pass
+        except SystemExit:
+            raise # fcgi uses this to terminate a thread
         except Exception, err:
             self.fail(err)
             self.finish()
 
         if self.cfg.log_timing:
-            self.timing_log(False, action)
+            self.timing_log(False, action_name)
 
         return self.finish()
 
--- a/MoinMoin/request/request_modpython.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/request/request_modpython.py	Sun Jan 06 23:39:24 2008 +0100
@@ -12,7 +12,7 @@
 class Request(RequestBase):
     """ specialized on mod_python requests """
 
-    def __init__(self, req):
+    def __init__(self, req, properties={}):
         """ Saves mod_pythons request and sets basic variables using
             the req.subprocess_env, cause this provides a standard
             way to access the values we need here.
@@ -33,7 +33,7 @@
             else:
                 env = req.subprocess_env
             self._setup_vars_from_std_env(env)
-            RequestBase.__init__(self)
+            RequestBase.__init__(self, properties)
 
         except Exception, err:
             self.fail(err)
@@ -84,7 +84,9 @@
         form = util.FieldStorage(self.mpyreq)
 
         args = {}
-        for key in form:
+
+        # You cannot get rid of .keys() here
+        for key in form.keys():
             if key is None:
                 continue
             values = form[key]
--- a/MoinMoin/script/migration/_conv160.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/script/migration/_conv160.py	Sun Jan 06 23:39:24 2008 +0100
@@ -53,8 +53,15 @@
     """ Convert the <text> content of page <pagename>, using <renames> dict
         to rename links correctly. Additionally, convert some changed markup.
     """
-    if "#format wiki" not in text and "#format" in text:
-        return text # this is not a wiki page, leave it as is
+    if text.startswith('<?xml'):
+        # would be done with xslt processor
+        return text
+
+    pis, body = wikiutil.get_processing_instructions(text)
+    for pi, val in pis:
+        if pi == 'format' and val != 'wiki':
+            # not wiki page
+            return text
 
     text = convert_wiki(request, pagename, text, renames)
     return text
@@ -141,6 +148,7 @@
             editlog = self.data.items()
             editlog.sort()
             f = file(fname, "w")
+            max_rev = 0
             for key, fields in editlog:
                 timestamp, rev, action, pagename, ip, hostname, userid, extra, comment = fields
                 if action.startswith('ATT'):
@@ -154,6 +162,8 @@
                 if ('PAGE', pagename) in self.renames:
                     pagename = self.renames[('PAGE', pagename)]
                 timestamp = str(timestamp)
+                if rev != 99999999:
+                    max_rev = max(rev, max_rev)
                 revstr = '%08d' % rev
                 pagename = wikiutil.quoteWikinameFS(pagename)
                 fields = timestamp, revstr, action, pagename, ip, hostname, userid, extra, comment
@@ -161,7 +171,7 @@
                 f.write(log_str)
             if create_rev and not deleted:
                 timestamp = str(wikiutil.timestamp2version(time.time()))
-                revstr = '%08d' % (rev + 1)
+                revstr = '%08d' % (max_rev + 1)
                 action = 'SAVE'
                 ip = '127.0.0.1'
                 hostname = 'localhost'
@@ -249,7 +259,11 @@
             current_file = file(current_fname, "r")
             current_rev = current_file.read()
             current_file.close()
-            self.current = int(current_rev)
+            try:
+                self.current = int(current_rev)
+            except ValueError:
+                print "Error: invalid current file %s, SKIPPING THIS PAGE!" % current_fname
+                return
         # read edit-log
         editlog_fname = opj(page_dir, 'edit-log')
         if os.path.exists(editlog_fname):
@@ -353,7 +367,11 @@
             line = line.replace(u'\r', '').replace(u'\n', '')
             if not line.strip() or line.startswith(u'#'): # skip empty or comment lines
                 continue
-            key, value = line.split(u'=', 1)
+            try:
+                key, value = line.split(u'=', 1)
+            except Exception, err:
+                print "Error: User reader can not parse line %r from profile %r (%s)" % (line, fname, str(err))
+                continue
             self.profile[key] = value
         f.close()
         # read bookmarks
--- a/MoinMoin/script/migration/_conv160_wiki.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/script/migration/_conv160_wiki.py	Sun Jan 06 23:39:24 2008 +0100
@@ -162,7 +162,13 @@
         macro_args = m.group('macro_args')
         if macro_name == 'ImageLink':
             fixed, kw, trailing = wikiutil.parse_quoted_separated(macro_args)
+            #print "macro_args=%r" % macro_args
+            #print "fixed=%r, kw=%r, trailing=%r" % (fixed, kw, trailing)
             image, target = (fixed + ['', ''])[:2]
+            if image is None:
+                image = ''
+            if target is None:
+                target = ''
             if '://' not in image:
                 # if it is not a URL, it is meant as attachment
                 image = u'attachment:%s' % image
--- a/MoinMoin/script/migration/migutil.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/script/migration/migutil.py	Sun Jan 06 23:39:24 2008 +0100
@@ -2,7 +2,7 @@
 """
     MoinMoin - utility functions used by the migration scripts
 
-    @copyright: 2005 MoinMoin:ThomasWaldmann
+    @copyright: 2005,2007 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 import os, sys, shutil
@@ -35,9 +35,9 @@
         fatalError("can't find '%s'. You must run this script from the directory where '%s' is located." % src)
 
     try:
-        os.rename(src, dst)
-    except OSError:
-        fatalError("can't rename '%s' to '%s'" % (src, dst))
+        shutil.move(src, dst)
+    except:
+        fatalError("can't move '%s' to '%s'" % (src, dst))
 
     try:
         os.mkdir(src)
@@ -66,18 +66,15 @@
     print "%s/ -> %s/" % (dir_from, dir_to)
     try:
         shutil.copytree(dir_from, dir_to)
-    except:
-        error("can't copy '%s' to '%s'" % (dir_from, dir_to))
+    except Exception, err:
+        error("can't copy '%s' to '%s' (%s)" % (dir_from, dir_to, str(err)))
 
 
 def copy_file(fname_from, fname_to):
     """ Copy a single file """
     print "%s -> %s" % (fname_from, fname_to)
     try:
-        data = open(fname_from).read()
-        open(fname_to, "w").write(data)
-        st = os.stat(fname_from)
-        os.utime(fname_to, (st.st_atime, st.st_mtime))
+        shutil.copy2(fname_from, fname_to) # copies file data, mode, atime, mtime
     except:
         error("can't copy '%s' to '%s'" % (fname_from, fname_to))
 
@@ -86,7 +83,7 @@
     """ Move a single file """
     print "%s -> %s" % (fname_from, fname_to)
     try:
-        os.rename(fname_from, fname_to)
+        shutil.move(fname_from, fname_to) # moves file (even to different filesystem, including mode and atime/mtime)
     except:
         error("can't move '%s' to '%s'" % (fname_from, fname_to))
 
--- a/MoinMoin/search/queryparser.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/search/queryparser.py	Sun Jan 06 23:39:24 2008 +0100
@@ -424,7 +424,8 @@
         return u'%s!"%s"' % (neg, unicode(self._pattern))
 
     def highlight_re(self):
-        return u"(%s)" % self._pattern
+        return u'' # do not highlight text with stuff from titlesearch,
+                   # was: return u"(%s)" % self._pattern
 
     def pageFilter(self):
         """ Page filter function for single title search """
--- a/MoinMoin/search/results.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/search/results.py	Sun Jan 06 23:39:24 2008 +0100
@@ -674,10 +674,11 @@
             return self.request.page.url(self.request, querydict,
                     escape=0, relative=False)
 
-        pages = float(hitsNum) / hitsPerPage
-        if pages - int(pages) > 0.0:
-            pages = int(pages) + 1
-        cur_page = hitsFrom / hitsPerPage
+        pages = hitsNum // hitsPerPage
+        remainder = hitsNum % hitsPerPage
+        if remainder:
+            pages += 1
+        cur_page = hitsFrom // hitsPerPage
 
         textlinks = []
 
@@ -741,9 +742,9 @@
         size_str = '%.1fk' % (p.size()/1024.0)
         revisions = p.getRevList()
         if len(revisions) and rev == revisions[0]:
-            rev_str = 'rev: %d (%s)' % (rev, _('current'))
+            rev_str = '%s: %d (%s)' % (_('rev'), rev, _('current'))
         else:
-            rev_str = 'rev: %d' % (rev, )
+            rev_str = '%s: %d' % (_('rev'), rev, )
         lastmod_str = _('last modified: %s') % p.mtime_printable(request)
 
         result = f.paragraph(1, attr={'class': 'searchhitinfobar'}) + \
@@ -759,7 +760,9 @@
         if querydict is None:
             querydict = {}
         if 'action' not in querydict or querydict['action'] == 'AttachFile':
-            querydict.update({'highlight': self.query.highlight_re()})
+            highlight = self.query.highlight_re()
+            if highlight:
+                querydict.update({'highlight': highlight})
         querystr = wikiutil.makeQueryString(querydict)
         return querystr
 
--- a/MoinMoin/security/antispam.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/security/antispam.py	Sun Jan 06 23:39:24 2008 +0100
@@ -9,8 +9,15 @@
 # give some log entries to stderr
 debug = 0
 
-import re, sys, time, datetime
-import sets
+import re, time, datetime
+
+# needed for py 2.3 compat:
+try:
+    frozenset
+except NameError:
+    from sets import ImmutableSet as frozenset
+
+import logging
 
 from MoinMoin.security import Permissions
 from MoinMoin import caching, wikiutil
@@ -42,7 +49,7 @@
     if debug:
         if isinstance(s, unicode):
             s = s.encode('utf-8')
-        sys.stderr.write('%s\n' % s)
+        logging.debug('antispam: %s' % s)
 
 
 def makelist(text):
@@ -157,7 +164,8 @@
             blacklist = []
             latest_mtime = 0
             for pn in BLACKLISTPAGES:
-                do_update = (pn != "LocalBadContent")
+                do_update = (pn != "LocalBadContent" and
+                             request.cfg.interwikiname != 'MoinMaster') # MoinMaster wiki shall not fetch updates from itself
                 blacklist_mtime, blacklist_entries = getblacklist(request, pn, do_update)
                 blacklist += blacklist_entries
                 latest_mtime = max(latest_mtime, blacklist_mtime)
@@ -180,10 +188,10 @@
                     page = Page(request, editor.page_name, rev=rev)
                     oldtext = page.get_raw_body()
 
-                newset = sets.ImmutableSet(newtext.splitlines(1))
-                oldset = sets.ImmutableSet(oldtext.splitlines(1))
-                difference = newset.difference(oldset)
-                addedtext = ''.join(difference)
+                newset = frozenset(newtext.splitlines(1))
+                oldset = frozenset(oldtext.splitlines(1))
+                difference = newset - oldset
+                addedtext = kw.get('comment', u'') + u''.join(difference)
 
                 for blacklist_re in request.cfg.cache.antispam_blacklist[1]:
                     match = blacklist_re.search(addedtext)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/security/textcha.py	Sun Jan 06 23:39:24 2008 +0100
@@ -0,0 +1,170 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Text CAPTCHAs
+    
+    This is just asking some (admin configured) questions and
+    checking if the answer is as expected. It is up to the wiki
+    admin to setup questions that a bot can not easily answer, but
+    humans can. It is recommended to setup SITE SPECIFIC questions
+    and not to share the questions with other sites (if everyone
+    asks the same questions / expects the same answers, spammers
+    could adapt to that).
+
+    TODO:
+    * roundtrip the question in some other way:
+     * use safe encoding / encryption for the q
+     * make sure a q/a pair in the POST is for the q in the GET before
+    * make some nice CSS
+    * make similar changes to GUI editor
+
+    @copyright: 2007 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import re
+import random
+import logging
+
+from MoinMoin import wikiutil
+
+class TextCha(object):
+    """ Text CAPTCHA support """
+
+    def __init__(self, request, question=None):
+        """ Initialize the TextCha.
+
+            @param request: the request object
+            @param question: see _init_qa()
+        """
+        self.request = request
+        self.user_info = request.user.valid and request.user.name or request.remote_addr
+        self.textchas = self._get_textchas()
+        self._init_qa(question)
+
+    def _get_textchas(self):
+        """ get textchas from the wiki config for the user's language (or default_language or en) """
+        request = self.request
+        cfg = request.cfg
+        user = request.user
+        disabled_group = cfg.textchas_disabled_group
+        if disabled_group and user.name and request.dicts.has_member(disabled_group, user.name):
+            return None
+        textchas = cfg.textchas
+        if textchas:
+            lang = user.language or request.lang
+            #logging.debug(u"TextCha: user.language == '%s'." % lang)
+            if lang not in textchas:
+                lang = cfg.language_default
+                #logging.debug(u"TextCha: fallback to language_default == '%s'." % lang)
+                if lang not in textchas:
+                    logging.error(u"TextCha: The textchas do not have content for language_default == '%s'! Falling back to English." % lang)
+                    lang = 'en'
+                    if lang not in textchas:
+                        logging.error(u"TextCha: The textchas do not have content for 'en', auto-disabling textchas!")
+                        cfg.textchas = None
+                        lang = None
+        else:
+            lang = None
+        if lang is None:
+            return None
+        else:
+            #logging.debug(u"TextCha: using lang = '%s'" % lang)
+            return textchas[lang]
+
+    def _init_qa(self, question=None):
+        """ Initialize the question / answer.
+        
+         @param question: If given, the given question will be used.
+                          If None, a new question will be generated.
+        """
+        if self.is_enabled():
+            if question is None:
+                self.question = random.choice(self.textchas.keys())
+            else:
+                self.question = question
+            try:
+                self.answer_regex = self.textchas[self.question]
+                self.answer_re = re.compile(self.answer_regex, re.U|re.I)
+            except KeyError:
+                # this question does not exist, thus there is no answer
+                self.answer_regex = ur"[^.]*" # this shall never match!
+                self.answer_re = re.compile(self.answer_regex, re.U|re.I)
+                logging.warning(u"TextCha: Non-existing question '%s'. User '%s' trying to cheat?" % (
+                                self.question, self.user_info))
+            except re.error:
+                logging.error(u"TextCha: Invalid regex in answer for question '%s'" % self.question)
+                self._init_qa()
+
+    def is_enabled(self):
+        """ check if textchas are enabled.
+
+            They can be disabled for all languages if you use textchas = None or = {},
+            also they can be disabled for some specific language, like:
+            textchas = {
+                'en': {
+                    'some question': 'some answer',
+                    # ...
+                },
+                'de': {}, # having no questions for 'de' means disabling textchas for 'de'
+                # ...
+            }             
+        """
+        return not not self.textchas # we don't want to return the dict
+
+    def check_answer(self, given_answer):
+        """ check if the given answer to the question is correct """
+        if self.is_enabled():
+            success = self.answer_re.match(given_answer.strip()) is not None
+            success_status = success and u"success" or u"failure"
+            logging.info(u"TextCha: %s (u='%s', a='%s', re='%s', q='%s')" % (
+                             success_status,
+                             self.user_info,
+                             given_answer,
+                             self.answer_regex,
+                             self.question,
+                             ))
+            return success
+        else:
+            return True
+
+    def _make_form_values(self, question, given_answer):
+        question_form = wikiutil.escape(question, True)
+        given_answer_form = wikiutil.escape(given_answer, True)
+        return question_form, given_answer_form
+
+    def _extract_form_values(self, form=None):
+        if form is None:
+            form = self.request.form
+        question = form.get('textcha-question', [None])[0]
+        given_answer = form.get('textcha-answer', [u''])[0]
+        return question, given_answer
+
+    def render(self, form=None):
+        """ Checks if textchas are enabled and returns HTML for one,
+            or an empty string if they are not enabled.
+            
+            @return: unicode result html         
+        """
+        if self.is_enabled():
+            question, given_answer = self._extract_form_values(form)
+            if question is None:
+                question = self.question
+            question_form, given_answer_form = self._make_form_values(question, given_answer)
+            result = u"""
+<div id="textcha">
+<span id="textcha-question">%s</span>
+<input type="hidden" name="textcha-question" value="%s">
+<input id="textcha-answer" type="text" name="textcha-answer" value="%s" size="20" maxlength="80">
+</div>
+""" % (wikiutil.escape(question), question_form, given_answer_form)
+        else:
+            result = u''
+        return result
+
+    def check_answer_from_form(self, form=None):
+        if self.is_enabled():
+            question, given_answer = self._extract_form_values(form)
+            self._init_qa(question)
+            return self.check_answer(given_answer)
+        else:
+            return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/server/server_modpython.py	Sun Jan 06 23:39:24 2008 +0100
@@ -0,0 +1,49 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin.server.server_modpython
+
+    This is not really a server, it is just so that modpython stuff
+    (the real server is likely Apache2) fits the model we have for
+    Twisted and standalone server.
+
+    Minimal usage:
+
+        from MoinMoin.server.server_modpython import CgiConfig, run
+        
+        class Config(CgiConfig):
+            pass
+
+        run(Config)
+        
+    See more options in CgiConfig class.
+
+    @copyright: 2006 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin.server import Config
+from MoinMoin.request import request_modpython
+
+# Set threads flag, so other code can use proper locking.
+# TODO: It seems that modpy does not use threads, so we don't need to
+# set it here. Do we have another method to check this?
+from MoinMoin import config
+config.use_threads = 1
+del config
+
+# Server globals
+config = None
+
+class ModpythonConfig(Config):
+    """ Set up default server """
+
+    logPath = None
+    properties = {}
+    
+    # Set up log handler to log to apache log!
+
+def modpythonHandler(request, ConfigClass=ModpythonConfig):
+    config = ConfigClass()
+    moinreq = request_modpython.Request(request, config.properties)
+    return moinreq.run(request)
+
--- a/MoinMoin/server/server_wsgi.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/server/server_wsgi.py	Sun Jan 06 23:39:24 2008 +0100
@@ -1,12 +1,31 @@
 """
     MoinMoin - WSGI application
 
-    @copyright: 2005 Anakim Border <akborder@gmail.com>
+    Minimal code for using this:
+
+    import logging
+    from MoinMoin.server.server_wsgi import WsgiConfig, moinmoinApp
+    
+    class Config(WsgiConfig):
+        logPath = 'moin.log' # define your log file here
+        #loglevel_file = logging.INFO # if you do not like the default
+
+    config = Config() # you MUST create an instance to initialize logging!
+    # use moinmoinApp here with your WSGI server / gateway
+
+    @copyright: 2005 Anakim Border <akborder@gmail.com>,
+                2007 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
+from MoinMoin.server import Config
 from MoinMoin.request import request_wsgi
 
+class WsgiConfig(Config):
+    """ WSGI default config """
+    loglevel_stderr = None # we do not want to write to stderr!
+         
+
 def moinmoinApp(environ, start_response):
     request = request_wsgi.Request(environ)
     request.run()
--- a/MoinMoin/stats/hitcounts.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/stats/hitcounts.py	Sun Jan 06 23:39:24 2008 +0100
@@ -8,16 +8,22 @@
           A lot of code here is duplicated in stats.useragents.
           Maybe both can use same base class, maybe some parts are useful to other code.
 
-    @copyright: 2002-2004 Juergen Hermann <jh@web.de>
+    @copyright: 2002-2004 Juergen Hermann <jh@web.de>,
+                2007 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
 _debug = 0
 
+import time
+
 from MoinMoin import caching, wikiutil, logfile
 from MoinMoin.Page import Page
 from MoinMoin.logfile import eventlog
 
+# this is a CONSTANT used for on-disk caching, it must NOT be configurable and
+# not depend on request.user!
+DATE_FMT = '%04d-%02d-%02d' # % (y, m, d)
 
 def linkto(pagename, request, params=''):
     _ = request.getText
@@ -52,14 +58,14 @@
     # Get results from cache
     if filterpage:
         arena = Page(request, pagename)
-        cache = caching.CacheEntry(request, arena, 'hitcounts', scope='item')
+        cache = caching.CacheEntry(request, arena, 'hitcounts', scope='item', use_pickle=True)
     else:
         arena = 'charts'
-        cache = caching.CacheEntry(request, arena, 'hitcounts', scope='wiki')
+        cache = caching.CacheEntry(request, arena, 'hitcounts', scope='wiki', use_pickle=True)
 
     if cache.exists():
         try:
-            cache_date, cache_days, cache_views, cache_edits = eval(cache.content())
+            cache_date, cache_days, cache_views, cache_edits = cache.content()
         except:
             cache.remove() # cache gone bad
 
@@ -79,34 +85,34 @@
     if new_date is not None:
         log.set_filter(['VIEWPAGE', 'SAVEPAGE'])
         for event in log.reverse():
-            #print ">>>", wikiutil.escape(repr(event)), "<br>"
-
-            if event[0] <= cache_date:
+            event_usecs = event[0]
+            if event_usecs <= cache_date:
                 break
             eventpage = event[2].get('pagename', '')
             if filterpage and eventpage != filterpage:
                 continue
-            time_tuple = request.user.getTime(wikiutil.version2timestamp(event[0]))
+            event_secs = wikiutil.version2timestamp(event_usecs)
+            time_tuple = time.gmtime(event_secs) # must be UTC
             day = tuple(time_tuple[0:3])
             if day != ratchet_day:
                 # new day
                 while ratchet_time:
-                    ratchet_time -= 86400
-                    rday = tuple(request.user.getTime(ratchet_time)[0:3])
+                    ratchet_time -= 86400 # seconds per day
+                    rday = tuple(time.gmtime(ratchet_time)[0:3]) # must be UTC
                     if rday <= day:
                         break
-                    days.append(request.user.getFormattedDate(ratchet_time))
+                    days.append(DATE_FMT % rday)
                     views.append(0)
                     edits.append(0)
-                days.append(request.user.getFormattedDate(wikiutil.version2timestamp(event[0])))
+                days.append(DATE_FMT % day)
                 views.append(0)
                 edits.append(0)
                 ratchet_day = day
-                ratchet_time = wikiutil.version2timestamp(event[0])
+                ratchet_time = event_secs
             if event[1] == 'VIEWPAGE':
-                views[-1] = views[-1] + 1
+                views[-1] += 1
             elif event[1] == 'SAVEPAGE':
-                edits[-1] = edits[-1] + 1
+                edits[-1] += 1
 
         days.reverse()
         views.reverse()
@@ -123,8 +129,7 @@
     cache_views.extend(views)
     cache_edits.extend(edits)
     if new_date is not None:
-        cache.update("(%r, %r, %r, %r)" %
-                     (new_date, cache_days, cache_views, cache_edits))
+        cache.update((new_date, cache_days, cache_views, cache_edits))
 
     return cache_days, cache_views, cache_edits
 
--- a/MoinMoin/stats/useragents.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/stats/useragents.py	Sun Jan 06 23:39:24 2008 +0100
@@ -7,7 +7,8 @@
 
     TODO: should be refactored after hitcounts.
 
-    @copyright: 2002-2004 Juergen Hermann <jh@web.de>
+    @copyright: 2002-2004 Juergen Hermann <jh@web.de>,
+                2007 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -45,11 +46,11 @@
 
 def get_data(request):
     # get results from cache
-    cache = caching.CacheEntry(request, 'charts', 'useragents', scope='wiki')
+    cache = caching.CacheEntry(request, 'charts', 'useragents', scope='wiki', use_pickle=True)
     cache_date, data = 0, {}
     if cache.exists():
         try:
-            cache_date, data = eval(cache.content())
+            cache_date, data = cache.content()
         except:
             cache.remove() # cache gone bad
 
@@ -75,7 +76,7 @@
                 data[ua] = data.get(ua, 0) + 1
 
         # write results to cache
-        cache.update("(%r, %r)" % (new_date, data))
+        cache.update((new_date, data))
 
     data = [(cnt, ua) for ua, cnt in data.items()]
     data.sort()
--- a/MoinMoin/support/thfcgi.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/support/thfcgi.py	Sun Jan 06 23:39:24 2008 +0100
@@ -11,7 +11,9 @@
 
     Cleanup, fixed typos, PEP-8, support for limiting creation of threads,
     limited number of requests lifetime, configurable backlog for socket
-    .listen() by Thomas Waldmann <tw AT waldmann-edv DOT de>
+    .listen() by MoinMoin:ThomasWaldmann.
+
+    2007 Support for Python's logging module by MoinMoin:ThomasWaldmann.
 
     For code base see:
     http://cvs.lysator.liu.se/viewcvs/viewcvs.cgi/webkom/thfcgi.py?cvsroot=webkom
@@ -33,7 +35,8 @@
 # TODO: Compare compare the number of bytes received on FCGI_STDIN with
 #       CONTENT_LENGTH and abort the update if the two numbers are not equal.
 
-debug = False
+import logging
+LOGLEVEL = logging.DEBUG # logging.NOTSET to completely switch it off
 
 import os
 import sys
@@ -96,12 +99,9 @@
 FCGI_UnknownTypeBody = "!B7x"
 FCGI_EndRequestBody = "!IB3x"
 
-LOGFILE = sys.stderr
 
 def log(s):
-    if debug:
-        LOGFILE.write(s)
-        LOGFILE.write('\n')
+    logging.log(LOGLEVEL, 'thfcgi: %s' % s)
 
 class SocketErrorOnWrite:
     """Is raised if a write fails in the socket code."""
@@ -584,11 +584,13 @@
     def run(self):
         """Wait & serve. Calls request_handler on every request."""
         self.sock.listen(self.backlog)
-        log("Starting Process")
+        pid = os.getpid()
+        log("Starting Process (PID=%d)" % pid)
         running = True
         while running:
             if not self.requests_left:
                 # self.sock.shutdown(RDWR) here does NOT help with backlog
+                log("Maximum number of processed requests reached, terminating this worker process (PID=%d)..." % pid)
                 running = False
             elif self.requests_left > 0:
                 self.requests_left -= 1
@@ -596,13 +598,12 @@
                 conn, addr = self.sock.accept()
                 threadcount = _threading.activeCount()
                 if threadcount < self.max_threads:
-                    log("Accepted connection, starting thread...")
+                    log("Accepted connection, %d active threads, starting worker thread..." % threadcount)
                     t = _threading.Thread(target=self.accept_handler, args=(conn, addr, True))
                     t.start()
                 else:
-                    log("Accepted connection, running in main-thread...")
+                    log("Accepted connection, %d active threads, running in main thread..." % threadcount)
                     self.accept_handler(conn, addr, False)
-                log("Active Threads: %d" % _threading.activeCount())
         self.sock.close()
-        log("Ending Process")
+        log("Ending Process (PID=%d)" % pid)
 
--- a/MoinMoin/theme/__init__.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/theme/__init__.py	Sun Jan 06 23:39:24 2008 +0100
@@ -223,7 +223,7 @@
         """
         _ = self.request.getText
         content = []
-        if d['title_text'] == d['page_name']: # just showing a page, no action
+        if d['title_text'] == d['page'].split_title(): # just showing a page, no action
             curpage = ''
             segments = d['page_name'].split('/') # was: title_text
             for s in segments[:-1]:
@@ -369,6 +369,8 @@
         else:
             page = Page(request, pagename)
 
+        pagename = page.page_name # can be different, due to i18n
+
         if not title:
             title = page.split_title()
             title = self.shortenPagename(title)
@@ -539,16 +541,18 @@
         @rtype: string
         @return: html link tag
         """
+        qs = {}
         querystr, title, icon = self.cfg.page_icons_table[which]
+        qs.update(querystr) # do not modify the querystr dict in the cfg!
         d['title'] = title % d
         d['i18ntitle'] = self.request.getText(d['title'], formatted=False)
         img_src = self.make_icon(icon, d)
         rev = d['rev']
         if rev and which in ['raw', 'print', ]:
-            querystr['rev'] = str(rev)
+            qs['rev'] = str(rev)
         attrs = {'rel': 'nofollow', 'title': d['i18ntitle'], }
         page = d['page']
-        return page.link_to_raw(self.request, text=img_src, querystr=querystr, **attrs)
+        return page.link_to_raw(self.request, text=img_src, querystr=qs, **attrs)
 
     def msg(self, d):
         """ Assemble the msg display
@@ -1113,8 +1117,12 @@
         """ Return whether the gui editor / converter can work for that page.
 
             The GUI editor currently only works for wiki format.
+            For simplicity, we also tell it does not work if the admin forces the text editor.
         """
-        return page.pi['format'] == 'wiki'
+        is_wiki = page.pi['format'] == 'wiki'
+        gui_disallowed = self.cfg.editor_force and self.cfg.editor_default == 'text'
+        return is_wiki and not gui_disallowed
+
 
     def editorLink(self, page):
         """ Return a link to the editor
--- a/MoinMoin/userform/admin.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/userform/admin.py	Sun Jan 06 23:39:24 2008 +0100
@@ -7,7 +7,7 @@
                 2007 MoinMoin:ReimarBauer
     @license: GNU GPL, see COPYING for details.
 """
-from MoinMoin import user, wikidicts
+from MoinMoin import user, wikidicts, wikiutil
 from MoinMoin.util.dataset import TupleDataset, Column
 from MoinMoin.Page import Page
 
@@ -18,7 +18,6 @@
 
     data = TupleDataset()
     data.columns = [
-        #Column('id', label=('ID'), align='right'),
         Column('name', label=_('Username')),
         Column('acl groups', label=_('ACL Groups')),
         Column('email', label=_('Email')),
@@ -44,29 +43,46 @@
         if userhomepage.exists():
             namelink = userhomepage.link_to(request)
         else:
-            namelink = account.name
+            namelink = wikiutil.escape(account.name)
+
+        if account.disabled:
+            enable_disable_link = request.page.link_to(
+                                    request, text=_('Enable user'),
+                                    querystr={"action":"userprofile",
+                                              "name": account.name,
+                                              "key": "disabled",
+                                              "val": "0",
+                                             },
+                                    rel='nofollow')
+            namelink += " (%s)" % _("disabled")
+        else:
+            enable_disable_link = request.page.link_to(
+                                    request, text=_('Disable user'),
+                                    querystr={"action":"userprofile",
+                                              "name": account.name,
+                                              "key": "disabled",
+                                              "val": "1",
+                                             },
+                                    rel='nofollow')
+
+        mail_link = request.page.link_to(
+                        request, text=_('Mail account data'),
+                        querystr={"action": "recoverpass",
+                                  "email": account.email,
+                                  "account_sendmail": "1",
+                                  "sysadm": "users", },
+                        rel='nofollow')
 
         data.addRow((
-            #request.formatter.code(1) + uid + request.formatter.code(0),
-            # 0
             request.formatter.rawHTML(namelink),
-            # 1
             request.formatter.rawHTML(list_groups),
-            # 2
             (request.formatter.url(1, 'mailto:' + account.email, css='mailto', do_escape=0) +
              request.formatter.text(account.email) +
              request.formatter.url(0)),
-            # 3
             (request.formatter.url(1, 'xmpp:' + account.jid, css='mailto', do_escape=0) +
              request.formatter.text(account.jid) +
              request.formatter.url(0)),
-            # 4
-            (request.page.link_to(request, text=_('Mail account data'),
-                                 querystr={"action": "recoverpass",
-                                           "email": account.email,
-                                           "account_sendmail": "1",
-                                           "sysadm": "users", },
-                                 rel='nofollow'))
+            mail_link + " - " + enable_disable_link
         ))
 
     if data:
--- a/MoinMoin/userprefs/changepass.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/userprefs/changepass.py	Sun Jan 06 23:39:24 2008 +0100
@@ -50,6 +50,8 @@
         # Check if password is given and matches with password repeat
         if password != password2:
             return _("Passwords don't match!")
+        if not password:
+            return _("Please specify a password!")
 
         pw_checker = request.cfg.password_checker
         if pw_checker:
--- a/MoinMoin/util/diff_html.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/util/diff_html.py	Sun Jan 06 23:39:24 2008 +0100
@@ -38,7 +38,7 @@
 
     if len(seq1) == len(seq2) and linematch[0] == (0, 0, len(seq1)):
         # No differences.
-        return _("No differences found!")
+        return " - " + _("No differences found!", formatted=False)
 
     lastmatch = (0, 0)
 
--- a/MoinMoin/wikiutil.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/MoinMoin/wikiutil.py	Sun Jan 06 23:39:24 2008 +0100
@@ -161,7 +161,10 @@
         s = s.encode(config.charset) # ascii would also work
     s = urllib.unquote(s)
     if want_unicode:
-        s = s.decode(config.charset)
+        try:
+            s = decodeUserInput(s, [config.charset, 'iso-8859-1', ]) # try hard
+        except UnicodeError:
+            s = s.decode('ascii', 'replace') # better than crashing
     return s
 
 def parseQueryString(qstr, want_unicode=True):
--- a/README	Sun Jan 06 23:38:45 2008 +0100
+++ b/README	Sun Jan 06 23:39:24 2008 +0100
@@ -50,59 +50,20 @@
 ACKNOWLEDGEMENTS
 ================
 
-We like to thank especially these people, groups and companies for their
-valuable ideas and contributions:
-
-    Ward Cunningham <ward@c2.com> for inventing WikiWiki in the first place.
-
-    Martin Pool <mbp@humbug.org.au> for providing the base of what now is
-    MoinMoin, see also docs/licenses/pikipiki.txt.
-
-    Marko Schulz <Marko.Schulz@gmx.de> for providing input on the first
-    versions of MoinMoin as it evolved.
-
-    Tom Holroyd <tomh@po.crl.go.jp> provided the set of emoticons that come
-    with the standard distribution, and came up with the idea to add that
-    feature.
-
-    Richard Jones <richard@bizarsoftware.com.au> who provided so much code
-    in so little time that I could not keep up with him. He works on company
-    time, though (unless I'm mistaken). Also found the nasty security hole
-    in release 0.6.
+We have to thank a lots of people for their valuable ideas, time and
+contributions - please see the MoinMoinAcknowledgements page there:
 
-    Christian Bird <chris.bird@lineo.com> provided some nice ideas and some
-    code.
-
-    Peter Thoeny and all other people involved in creating TWikiDrawPlugin;
-    for copyright information see docs/licenses/twikidraw.txt; for more
-    information and a download link to get the source refer to
-    http://twiki.org/cgi-bin/view/TWiki/TWikiDrawPlugin
-
-    Thomas Waldmann for the major effort of translating all the help and
-    system pages to German, and increasing volumes of code.
-
-    Bernhard Rosenkraenzer and Andrew Bennetts for password login patches.
-
-    Florian Festi, Bastian Blank, Oliver Graf and Nir Soffer for many code
-    contributions.
-
-    Alexander Schremmer for much testing work, code contributions and
-    finally fixing the win32 stuff.
-
-    Arcelor S.A. for sponsoring MoinMoin v1.5 GPL development.
-
-    All language maintainers for translating MoinMoin to many languages and
-    keeping those translations up-to-date.
-
-    And all the people we forgot or that are mentioned in the code.
+http://moinmo.in/MoinMoinAcknowledgements
 
 
------------------------------------------------------------------------------
+LICENSE NOTES
+=============
 
-The slides in wiki\data\text\WikiSchulung* were written at WEB.DE AG and
-licensed to MoinMoin users under the terms of "IFL-Text v1.0".
+Martin Pool <mbp@humbug.org.au> provided the base of what now is MoinMoin,
+see also docs/licenses/pikipiki.txt.
 
-Copyright (c) 2003 by WEB.DE AG, Karlsruhe, Germany.
+Peter Thoeny and all other people involved in creating it provided
+TWikiDrawPlugin (see docs/licenses/twikidraw.txt). For more information
+and a download link to get the source refer to:
+http://twiki.org/cgi-bin/view/TWiki/TWikiDrawPlugin
 
-IFL-Text v1.0: see http://www.ifross.de/ifross_html/ifl.html
-
--- a/wiki/htdocs/classic/css/screen.css	Sun Jan 06 23:38:45 2008 +0100
+++ b/wiki/htdocs/classic/css/screen.css	Sun Jan 06 23:39:24 2008 +0100
@@ -361,6 +361,20 @@
 	background: url(../img/draft.png);
 }
 
+#textcha {
+    font-size: 100%;
+    margin-top: 0.5em;
+    border: 2px solid #FF8888;
+    color: black;
+    vertical-align: middle;
+    padding: 3px 2px;
+}
+
+#textcha-answer {
+    border: 2px solid #000000;
+    padding: 3px 2px;
+}
+
 .diff {
 	width:99%;
 }
--- a/wiki/htdocs/modern/css/screen.css	Sun Jan 06 23:38:45 2008 +0100
+++ b/wiki/htdocs/modern/css/screen.css	Sun Jan 06 23:39:24 2008 +0100
@@ -383,6 +383,20 @@
     margin-top: 0.5em;
 }
 
+#textcha {
+    font-size: 100%;
+    margin-top: 0.5em;
+    border: 2px solid #FF8888;
+    color: black;
+    vertical-align: middle;
+    padding: 3px 2px;
+}
+
+#textcha-answer {
+    border: 2px solid #000000;
+    padding: 3px 2px;
+}
+
 input.button {
     /*
     border: 1px solid #8cacbb;  
--- a/wiki/htdocs/rightsidebar/css/screen.css	Sun Jan 06 23:38:45 2008 +0100
+++ b/wiki/htdocs/rightsidebar/css/screen.css	Sun Jan 06 23:39:24 2008 +0100
@@ -289,6 +289,20 @@
 	background: url(../img/draft.png);
 }
 
+#textcha {
+    font-size: 100%;
+    margin-top: 0.5em;
+    border: 2px solid #FF8888;
+    color: black;
+    vertical-align: middle;
+    padding: 3px 2px;
+}
+
+#textcha-answer {
+    border: 2px solid #000000;
+    padding: 3px 2px;
+}
+
 #pagebottom {
 	clear: both;
 }
--- a/wiki/server/moin.fcg	Sun Jan 06 23:38:45 2008 +0100
+++ b/wiki/server/moin.fcg	Sun Jan 06 23:39:24 2008 +0100
@@ -3,61 +3,38 @@
 """
     MoinMoin - FastCGI Driver Script
     
-    TODO: this should be refactored so it uses MoinMoin.server package
-          (see how Twisted, WSGI and Standalone use it)
-
-    @copyright: 2004-2005 by Oliver Graf <ograf@bitart.de>
+    @copyright: 2007 MoinMoin:ThomasWaldmann
     @license: GNU GPL, see COPYING for details.
 """
 
-# System path configuration
+import sys, logging
 
-import sys
+# Path to MoinMoin package, needed if you installed with --prefix=PREFIX
+# or if you did not use setup.py.
+#sys.path.insert(0, 'PREFIX/lib/python2.3/site-packages')
 
 # Path of the directory where wikiconfig.py is located.
 # YOU NEED TO CHANGE THIS TO MATCH YOUR SETUP.
 sys.path.insert(0, '/path/to/wikiconfig')
 
-# Path to MoinMoin package, needed if you installed with --prefix=PREFIX
-# or if you did not use setup.py.
-## sys.path.insert(0, 'PREFIX/lib/python2.3/site-packages')
-
 # Path of the directory where farmconfig is located (if different).
-## sys.path.insert(0, '/path/to/farmconfig')
+#sys.path.insert(0, '/path/to/farmconfig')
 
 # Debug mode - show detailed error reports
-## import os
-## os.environ['MOIN_DEBUG'] = '1'
-
-# how many requests shall be handled by a moin fcgi process before it dies,
-# -1 mean "unlimited lifetime":
-max_requests = -1
-
-# how many threads to use (1 means use only main program, non-threaded)
-max_threads = 5
-
-# backlog, use in socket.listen(backlog) call
-backlog = 5
-
-
-# Code ------------------------------------------------------------------
+#import os
+#os.environ['MOIN_DEBUG'] = '1'
 
-# Do not touch unless you know what you are doing!
-# TODO: move to server package?
-
-# Set threads flag, so other code can use proper locking
-from MoinMoin import config
-config.use_threads = max_threads > 1
-del config
+from MoinMoin.server.server_fastcgi import FastCgiConfig, run
 
-from MoinMoin.request import request_fcgi
-from MoinMoin.support import thfcgi
+class Config(FastCgiConfig):
+    loglevel_file = logging.DEBUG
+    logPath = 'moin.log'
 
-def handle_request(req, env, form):
-    request = request_fcgi.Request(req, env, form)
-    request.run()
+    properties = {}
+    # properties = {'script_name': '/'} # use this instead of the line above if your wiki runs under "/" url
 
-if __name__ == '__main__':
-    fcg = thfcgi.FCGI(handle_request, max_requests=max_requests, backlog=backlog, max_threads=max_threads)
-    fcg.run()
+    # for backlog, we use a default of 5. if the listen(backlog) call crashes for you, try a smaller value!
+    # backlog = 1
 
+run(Config)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/server/moin.wsgi	Sun Jan 06 23:39:24 2008 +0100
@@ -0,0 +1,48 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - mod_wsgi driver script
+
+    To use this, add those statements to your Apache's VirtualHost definition:
+    
+    # this is for icons, css, js (and must match url_prefix from wiki config):
+    Alias       /moin_static160/ /usr/share/moin/htdocs/
+
+    # this is the URL http://servername/moin/ you will use later to invoke moin:
+    WSGIScriptAlias /moin/ /some/path/moin.wsgi
+
+    # create some wsgi daemons - use someuser.somegroup same as your data_dir:
+    WSGIDaemonProcess daemonname user=someuser group=somegroup processes=5 threads=10 maximum-requests=1000
+    # umask=0007 does not work for mod_wsgi 1.0rc1, but will work later
+
+    # use the daemons we defined above to process requests!
+    WSGIProcessGroup daemonname
+
+    @copyright: 2007 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import sys
+
+# Path to MoinMoin package, needed if you installed with --prefix=PREFIX
+# or if you did not use setup.py.
+## sys.path.insert(0, 'PREFIX/lib/python2.3/site-packages')
+
+# Path of the directory where farmconfig.py is located (if different).
+## sys.path.insert(0, '/path/to/farmconfig')
+
+# Path of the directory where wikiconfig.py is located.
+# YOU NEED TO CHANGE THIS TO MATCH YOUR SETUP.
+sys.path.insert(0, '/path/to/wikiconfig')
+
+import logging
+
+from MoinMoin.server.server_wsgi import WsgiConfig, moinmoinApp
+
+class Config(WsgiConfig):
+    logPath = 'moin.log' # adapt this to your needs!
+    #loglevel_file = logging.INFO # adapt this to your needs!
+
+config = Config() # MUST create an instance to init logging!
+
+application = moinmoinApp
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wiki/server/moin_flup_wsgi.py	Sun Jan 06 23:39:24 2008 +0100
@@ -0,0 +1,33 @@
+"""
+    MoinMoin - Moin as WSGI application with flup as fcgi gateway
+
+    @copyright: 2005 by Anakim Border <akborder@gmail.com>,
+                2007 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+use_threads = True
+unixSocketPath = '/tmp/moin.sock'
+
+import os
+import logging
+
+# Set threads flag, so other code can use proper locking
+from MoinMoin import config
+config.use_threads = use_threads
+del config
+
+from flup.server.fcgi import WSGIServer
+from MoinMoin.server.server_wsgi import moinmoinApp, WsgiConfig
+
+class Config(WsgiConfig):
+    logPath = 'moin.log' # adapt to your needs!
+    #loglevel_file = logging.INFO # adapt to your needs!
+
+config = Config() # MUST create an instance to init logging
+
+if __name__ == '__main__':
+    server = WSGIServer(moinmoinApp, bindAddress=unixSocketPath)
+    server.run()
+    os.unlink(unixSocketPath)
+
--- a/wiki/server/moinmodpy.htaccess	Sun Jan 06 23:38:45 2008 +0100
+++ b/wiki/server/moinmodpy.htaccess	Sun Jan 06 23:39:24 2008 +0100
@@ -39,6 +39,6 @@
 #  PythonPath "['/path/to/moin/lib/python','/path/to/wikiconfig']+sys.path"
 #
 #  # choose the ModPy Request class as handler
-#  PythonHandler MoinMoin.request.MODPYTHON::Request.run
+#  PythonHandler MoinMoin.request.request_modpython::Request.run
 #
 #</Files>
--- a/wiki/server/moinmodpy.py	Sun Jan 06 23:38:45 2008 +0100
+++ b/wiki/server/moinmodpy.py	Sun Jan 06 23:39:24 2008 +0100
@@ -48,17 +48,23 @@
 ## import os
 ## os.environ['MOIN_DEBUG'] = '1'
 
-# Set threads flag, so other code can use proper locking.
-# TODO: It seems that modpy does not use threads, so we don't need to
-# set it here. Do we have another method to check this?
-from MoinMoin import config
-config.use_threads = 1
-del config
+# Simple way
+#from MoinMoin.server.server_modpython import modpythonHandler as handler
 
+# Complex way
+from MoinMoin.server.server_modpython import ModpythonConfig, modpythonHandler
 
-from MoinMoin.request import request_modpython
+class MyConfig(ModpythonConfig):
+    """ Set up local server-specific stuff here """
+
+    # Make sure moin will have permission to write to this file!
+    # Otherwise it will cause a server error.
+    logPath = "/var/log/apache2/moinlog"
+    
+    # Properties
+    # Allow overriding any request property by the value defined in
+    # this dict e.g properties = {'script_name': '/mywiki'}.
+    ## properties = {}
 
 def handler(request):
-    moinreq = request_modpython.Request(request)
-    return moinreq.run(request)
-
+    return modpythonHandler(request, MyConfig)
--- a/wiki/server/moinwsgi.py	Sun Jan 06 23:38:45 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-"""
-    MoinMoin - WSGI application
-
-    @copyright: 2005 by Anakim Border <akborder@gmail.com>
-    @license: GNU GPL, see COPYING for details.
-"""
-
-use_threads = True
-unixSocketPath = '/tmp/moin.sock'
-
-# Set threads flag, so other code can use proper locking
-from MoinMoin import config
-config.use_threads = use_threads
-del config
-
-from flup.server.fcgi import WSGIServer
-from MoinMoin.server.server_wsgi import moinmoinApp
-import os
-
-if __name__ == '__main__':
-    server = WSGIServer(moinmoinApp, bindAddress=unixSocketPath)
-    server.run()
-    os.unlink(unixSocketPath)
-