changeset 5524:069f75c3d59c

merged moin/1.8 + changes needed for ticket support of 1.9 drawings code
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Mon, 08 Feb 2010 21:49:19 +0100
parents ace26ca9c562 (diff) af66afbc9a31 (current diff)
children 5741e2608404
files MoinMoin/action/AttachFile.py MoinMoin/action/anywikidraw.py MoinMoin/action/twikidraw.py MoinMoin/support/python_compatibility.py MoinMoin/wikiutil.py
diffstat 2188 files changed, 199426 insertions(+), 118805 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Feb 08 19:01:03 2010 +0100
+++ b/.hgignore	Mon Feb 08 21:49:19 2010 +0100
@@ -4,9 +4,15 @@
 ^wiki/underlay/
 ^wiki/data/edit-log
 ^wiki/data/event-log
+^wiki/data/user/
 ^wiki/data/cache/
 ^wikiconfig_local.*
+^wikiserverconfig_local.*
 ^MoinMoin/i18n/POTFILES(\.in)?$
 .coverage
+^.project
+^.pydevproject
+^.settings
+^MANIFEST
 .DS_Store
 
--- a/.hgtags	Mon Feb 08 19:01:03 2010 +0100
+++ b/.hgtags	Mon Feb 08 21:49:19 2010 +0100
@@ -37,3 +37,11 @@
 3010c1a941856920ee564297f16570126b0231c0 1.8.4
 294b97b991d3b394aa7cf16ce18b01d8a64e6ef0 1.8.5
 137fcd650f26acbca8a964e0ed21aa378747b71c 1.8.6
+d706f5d4f4ecc935a69b0c6c5b90d47a643e82c4 1.9.0beta1
+a04008fe123371f144707ac237196fd7cc37ae90 1.9.0beta2
+47679e758f79d215bd748d2f1a3ec48f46dbadb3 1.9.0beta3
+f105598176f5be79b55a6a131f6936be8b66ca74 1.9.0beta4
+00ca621ffbc25413a6c81f9144edf34b283de606 1.9.0rc1
+9161bf60a745e979b90e04cec7b1507d0b94dd48 1.9.0rc2
+006173cad39c31b1d447418c049fd03ee9928aa0 1.9.0
+1ecb884c0deb7d73aa743a0cd1ea0875935f9c2d 1.9.1
--- a/MANIFEST.in	Mon Feb 08 19:01:03 2010 +0100
+++ b/MANIFEST.in	Mon Feb 08 21:49:19 2010 +0100
@@ -1,6 +1,6 @@
 # MoinMoin - Distutils distribution files
 #
-# Copyright (c) 2001, 2002 by Jürgen Hermann <jh@web.de>
+# Copyright (c) 2001, 2002 by Juergen Hermann <jh@web.de>
 # All rights reserved, see COPYING for details.
 
 # additional files not known by setup.py
@@ -13,6 +13,12 @@
 # include stuff for translators
 recursive-include   MoinMoin/i18n *
 
+# include static htdocs
+recursive-include   MoinMoin/web/static/htdocs *
+
+# include non-py stuff from werkzeug
+recursive-include   MoinMoin/support/werkzeug/debug *
+
 # contrib stuff
 recursive-include   contrib *
 
--- a/Makefile	Mon Feb 08 19:01:03 2010 +0100
+++ b/Makefile	Mon Feb 08 21:49:19 2010 +0100
@@ -13,33 +13,25 @@
 
 install-docs:
 	-mkdir build
-	wget -U MoinMoin/Makefile -O build/INSTALL.html "http://master18.moinmo.in/MoinMoin/InstallDocs?action=print"
+	wget -U MoinMoin/Makefile -O build/INSTALL.html "http://master19.moinmo.in/InstallDocs?action=print"
 	sed \
-		-e 's#href="/#href="http://master18.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' \
-		-e 's#/wiki/classic/#wiki/htdocs/classic/#g' \
-		-e 's#/wiki/modern/#wiki/htdocs/modern/#g' \
-		-e 's#/wiki/rightsidebar/#wiki/htdocs/rightsidebar/#g' \
+		-e 's#href="/#href="http://master19.moinmo.in/#g' \
+		-e 's#http://master19.moinmo.in/moin_static.../#../MoinMoin/web/static/htdocs/#g' \
+		-e 's#http://static.moinmo.in/moin_static.../#../MoinMoin/web/static/htdocs/#g' \
         build/INSTALL.html >docs/INSTALL.html
 	-rm build/INSTALL.html
 
-	wget -U MoinMoin/Makefile -O build/UPDATE.html "http://master18.moinmo.in/HelpOnUpdating?action=print"
+	wget -U MoinMoin/Makefile -O build/UPDATE.html "http://master19.moinmo.in/HelpOnUpdating?action=print"
 	sed \
-		-e 's#href="/#href="http://master18.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' \
-		-e 's#/wiki/classic/#wiki/htdocs/classic/#g' \
-		-e 's#/wiki/modern/#wiki/htdocs/modern/#g' \
-		-e 's#/wiki/rightsidebar/#wiki/htdocs/rightsidebar/#g' \
+		-e 's#href="/#href="http://master19.moinmo.in/#g' \
+		-e 's#http://master19.moinmo.in/moin_static.../#../MoinMoin/web/static/htdocs/#g' \
+		-e 's#http://static.moinmo.in/moin_static.../#../MoinMoin/web/static/htdocs/#g' \
         build/UPDATE.html >docs/UPDATE.html
 	-rm build/UPDATE.html
 	-rmdir build
 
 interwiki:
-	wget -U MoinMoin/Makefile -O $(share)/data/intermap.txt "http://master18.moinmo.in/InterWikiMap?action=raw"
+	wget -U MoinMoin/Makefile -O $(share)/data/intermap.txt "http://master19.moinmo.in/InterWikiMap?action=raw"
 	chmod 664 $(share)/data/intermap.txt
 
 check-tabs:
@@ -47,26 +39,30 @@
 
 # Create documentation
 epydoc: patchlevel
-	@epydoc -o ../html-1.8 --name=MoinMoin --url=http://moinmo.in/ --graph=all --graph-font=Arial MoinMoin
+	@epydoc --parse-only -o ../html-1.9 --name=MoinMoin --url=http://moinmo.in/ MoinMoin
 
 # Create new underlay directory from MoinMaster
 # Should be used only on TW machine
 underlay:
 	rm -rf $(share)/underlay
-	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.8 --wiki-url=master18.moinmo.in/ maint globaledit
-	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.8 --wiki-url=master18.moinmo.in/ maint reducewiki --target-dir=$(share)/underlay
+	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.9 --wiki-url=http://master19.moinmo.in/ maint globaledit
+	MoinMoin/script/moin.py --config-dir=/srv/moin/cfg/1.9 --wiki-url=http://master19.moinmo.in/ maint reducewiki --target-dir=$(share)/underlay
 	rm -rf $(share)/underlay/pages/InterWikiMap
 	rm -rf $(share)/underlay/pages/MoinPagesEditorGroup
 	cd $(share); rm -f underlay.tar; tar cf underlay.tar underlay
 
 pagepacks:
 	@python MoinMoin/_tests/maketestwiki.py
-	@MoinMoin/script/moin.py --config-dir=MoinMoin/_tests maint mkpagepacks
+	@MoinMoin/script/moin.py --config-dir=MoinMoin/_tests --wiki-url=http://localhost/ maint mkpagepacks
 	cd $(share) ; rm -rf underlay
 	cp -a $(testwiki)/underlay $(share)/
 	
 dist:
 	-rm MANIFEST
+	-rm -rf tests/wiki
+	-rm -rf wiki/data/cache/{__metalock__,__session__,wikiconfig}
+	->wiki/data/event-log
+	->wiki/data/edit-log
 	python setup.py sdist
 
 # Create patchlevel module
--- a/MoinMoin/Page.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/Page.py	Mon Feb 08 21:49:19 2010 +0100
@@ -109,7 +109,8 @@
             (for 'meta') or the complete cache ('pagelists').
             @param request: the request object
         """
-        elog = request.editlog
+        from MoinMoin.logfile import editlog
+        elog = editlog.EditLog(request)
         old_pos = self.log_pos
         new_pos, items = elog.news(old_pos)
         if items:
@@ -753,6 +754,9 @@
                 url = "%s/%s/%s" % (request.cfg.url_prefix_action, action, url)
             url = '%s?%s' % (url, querystr)
 
+        if not relative:
+            url = '%s/%s' % (request.script_root, url)
+
         # Add anchor
         if anchor:
             fmt = getattr(self, 'formatter', request.html_formatter)
@@ -760,8 +764,6 @@
                 anchor = fmt.sanitize_to_id(anchor)
             url = "%s#%s" % (url, anchor)
 
-        if not relative:
-            url = '%s/%s' % (request.getScriptname(), url)
         return url
 
     def link_to_raw(self, request, text, querystr=None, anchor=None, **kw):
@@ -959,33 +961,33 @@
         pi['acl'] = security.AccessControlList(request.cfg, acl)
         return pi
 
-    def send_raw(self, content_disposition=None):
+    def send_raw(self, content_disposition=None, mimetype=None):
         """ Output the raw page data (action=raw).
             With no content_disposition, the browser usually just displays the
             data on the screen, with content_disposition='attachment', it will
             offer a dialogue to save it to disk (used by Save action).
+            Supplied mimetype overrides default text/plain.
         """
         request = self.request
-        request.setHttpHeader("Content-type: text/plain; charset=%s" % config.charset)
+        request.mimetype = mimetype or 'text/plain'
         if self.exists():
             # use the correct last-modified value from the on-disk file
             # to ensure cacheability where supported. Because we are sending
             # RAW (file) content, the file mtime is correct as Last-Modified header.
-            request.setHttpHeader("Status: 200 OK")
-            request.setHttpHeader("Last-Modified: %s" % util.timefuncs.formathttpdate(os.path.getmtime(self._text_filename())))
+            request.status_code = 200
+            request.last_modified = os.path.getmtime(self._text_filename())
             text = self.encodeTextMimeType(self.body)
             #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
                 filename_enc = "%s.txt" % self.page_name.encode(config.charset)
-                request.setHttpHeader('Content-Disposition: %s; filename="%s"' % (
-                                      content_disposition, filename_enc))
+                dispo_string = '%s; filename="%s"' % (content_disposition, filename_enc)
+                request.headers.add('Content-Disposition', dispo_string)
         else:
-            request.setHttpHeader('Status: 404 NOTFOUND')
+            request.status_code = 404
             text = u"Page %s not found." % self.page_name
 
-        request.emit_http_headers()
         request.write(text)
 
     def send_page(self, **keywords):
@@ -1010,11 +1012,11 @@
         send_special = keywords.get('send_special', False)
         print_mode = keywords.get('print_mode', 0)
         if print_mode:
-            media = 'media' in request.form and request.form['media'][0] or 'print'
+            media = request.values.get('media', 'print')
         else:
             media = 'screen'
         self.hilite_re = (keywords.get('hilite_re') or
-                          request.form.get('highlight', [None])[0])
+                          request.values.get('highlight'))
 
         # count hit?
         if keywords.get('count_hit', 0):
@@ -1025,7 +1027,7 @@
         pi = self.pi
 
         if 'redirect' in pi and not (
-            'action' in request.form or 'redirect' in request.form or content_only):
+            'action' in request.values or 'redirect' in request.values or content_only):
             # redirect to another page
             # note that by including "action=show", we prevent endless looping
             # (see code in "request") or any cascaded redirection
@@ -1054,8 +1056,6 @@
             try:
                 self.formatter.set_highlight_re(self.hilite_re)
             except re.error, err:
-                if 'highlight' in request.form:
-                    del request.form['highlight']
                 request.theme.add_msg(_('Invalid highlighting regular expression "%(regex)s": %(error)s') % {
                                           'regex': self.hilite_re,
                                           'error': str(err),
@@ -1065,12 +1065,12 @@
         if 'deprecated' in pi:
             # deprecated page, append last backup version to current contents
             # (which should be a short reason why the page is deprecated)
-            request.theme.add_msg(_('The backed up content of this page is deprecated and will not be included in search results!'), "warning")
+            request.theme.add_msg(_('The backed up content of this page is deprecated and will rank lower in search results!'), "warning")
 
             revisions = self.getRevList()
             if len(revisions) >= 2: # XXX shouldn't that be ever the case!? Looks like not.
                 oldpage = Page(request, self.page_name, rev=revisions[1])
-                body += oldpage.get_raw_body()
+                body += oldpage.get_data()
                 del oldpage
 
         lang = self.pi.get('language', request.cfg.language_default)
@@ -1080,12 +1080,12 @@
         page_exists = self.exists()
         if not content_only:
             if emit_headers:
-                request.setHttpHeader("Content-Type: %s; charset=%s" % (self.output_mimetype, self.output_charset))
+                request.content_type = "%s; charset=%s" % (self.output_mimetype, self.output_charset)
                 if page_exists:
                     if not request.user.may.read(self.page_name):
-                        request.setHttpHeader('Status: 403 Permission Denied')
+                        request.status_code = 403
                     else:
-                        request.setHttpHeader('Status: 200 OK')
+                        request.status_code = 200
                     if not request.cacheable:
                         # use "nocache" headers if we're using a method that is not simply "display"
                         request.disableHttpCaching(level=2)
@@ -1100,8 +1100,7 @@
                         #request.setHttpHeader("Last-Modified: %s" % util.timefuncs.formathttpdate(lastmod))
                         pass
                 else:
-                    request.setHttpHeader('Status: 404 NOTFOUND')
-                request.emit_http_headers()
+                    request.status_code = 404
 
             if not page_exists and self.request.isSpiderAgent:
                 # don't send any 404 content to bots
@@ -1120,8 +1119,8 @@
 
                 # This redirect message is very annoying.
                 # Less annoying now without the warning sign.
-                if 'redirect' in request.form:
-                    redir = request.form['redirect'][0]
+                if 'redirect' in request.values:
+                    redir = request.values['redirect']
                     request.theme.add_msg('<strong>%s</strong><br>' % (
                         _('Redirected from page "%(page)s"') % {'page':
                             wikiutil.link_tag(request, wikiutil.quoteWikinameURL(redir) + "?action=show", self.formatter.text(redir))}), "info")
@@ -1146,12 +1145,11 @@
                         openid_username = self.pi['openid.user']
                         userid = user.getUserId(request, openid_username)
 
-                    if request.cfg.openid_server_restricted_users_group:
-                        request.dicts.addgroup(request,
-                                               request.cfg.openid_server_restricted_users_group)
-
-                    if userid is not None and not request.cfg.openid_server_restricted_users_group or \
-                      request.dicts.has_member(request.cfg.openid_server_restricted_users_group, openid_username):
+                    openid_group_name = request.cfg.openid_server_restricted_users_group
+                    if userid is not None and (
+                        not openid_group_name or (
+                            openid_group_name in request.groups and
+                            openid_username in request.groups[openid_group_name])):
                         html_head = '<link rel="openid2.provider" href="%s">' % \
                                         wikiutil.escape(request.getQualifiedURL(self.url(request,
                                                                                 querystr={'action': 'serveopenid'})), True)
@@ -1604,12 +1602,6 @@
 
         return Page(self.request, self.page_name, rev=lastRevision).parseACL()
 
-    def clean_acl_cache(self):
-        """
-        Clean ACL cache entry of this page (used by PageEditor on save)
-        """
-        pass # should not be necessary any more as the new cache watches edit-log for changes
-
     # Text format -------------------------------------------------------
 
     def encodeTextMimeType(self, text):
@@ -1868,9 +1860,7 @@
             # WARNING: SLOW
             pages = self.getPageList(user='')
         else:
-            pages = self.request.pages
-            if not pages:
-                pages = self._listPages()
+            pages = self._listPages()
         count = len(pages)
         self.request.clock.stop('getPageCount')
 
--- a/MoinMoin/PageEditor.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/PageEditor.py	Mon Feb 08 21:49:19 2010 +0100
@@ -197,12 +197,11 @@
 
         # Emit http_headers after checks (send_page)
         request.disableHttpCaching(level=2)
-        request.emit_http_headers()
 
         # check if we want to load a draft
         use_draft = None
         if 'button_load_draft' in form:
-            wanted_draft_timestamp = int(form.get('draft_ts', ['0'])[0])
+            wanted_draft_timestamp = int(form.get('draft_ts', '0'))
             if wanted_draft_timestamp:
                 draft = self._load_draft()
                 if draft is not None:
@@ -214,7 +213,7 @@
         if use_draft is not None:
             title = _('Draft of "%(pagename)s"')
             # Propagate original revision
-            rev = int(form['draft_rev'][0])
+            rev = int(form['draft_rev'])
             self.set_raw_body(use_draft, modified=1)
             preview = use_draft
         elif preview is None:
@@ -234,7 +233,7 @@
 
         # get request parameters
         try:
-            text_rows = int(form['rows'][0])
+            text_rows = int(form['rows'])
         except StandardError:
             text_rows = self.cfg.edit_rows
             if request.user.valid:
@@ -276,9 +275,9 @@
             # If the page exists, we get the text from the page.
             # TODO: maybe warn if template argument was ignored because the page exists?
             raw_body = self.get_raw_body()
-        elif 'template' in form:
+        elif 'template' in request.values:
             # If the page does not exist, we try to get the content from the template parameter.
-            template_page = wikiutil.unquoteWikiname(form['template'][0])
+            template_page = wikiutil.unquoteWikiname(request.values['template'])
             if request.user.may.read(template_page):
                 raw_body = Page(request, template_page).get_raw_body()
                 if raw_body:
@@ -332,10 +331,9 @@
             raw_body = _('Describe %s here.') % (self.page_name, )
 
         # send form
-        request.write('<form id="editor" method="post" action="%s/%s#preview" onSubmit="flgChange = false;">' % (
-            request.getScriptname(),
-            wikiutil.quoteWikinameURL(self.page_name),
-            ))
+        request.write('<form id="editor" method="post" action="%s#preview" onSubmit="flgChange = false;">' % (
+                request.href(self.page_name)
+        ))
 
         # yet another weird workaround for broken IE6 (it expands the text
         # editor area to the right after you begin to type...). IE sucks...
@@ -351,7 +349,7 @@
         request.write('<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket(request))
 
         # Save backto in a hidden input
-        backto = form.get('backto', [None])[0]
+        backto = request.values.get('backto')
         if backto:
             request.write(unicode(html.INPUT(type="hidden", name="backto", value=backto)))
 
@@ -410,7 +408,7 @@
     document.write('<label for="chktrivialtop">%(label)s</label>');
     //-->
 </script> ''' % {
-                'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'],
+                'checked': ('', 'checked')[form.get('trivial', '0') == '1'],
                 'label': _("Trivial change"),
             })
 
@@ -460,7 +458,7 @@
 <label for="chktrivial">%(label)s</label>
 
 ''' % {
-                'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'],
+                'checked': ('', 'checked')[form.get('trivial', '0') == '1'],
                 'label': _("Trivial change"),
                 })
 
@@ -469,7 +467,7 @@
 <input type="checkbox" name="rstrip" id="chkrstrip" value="1" %(checked)s>
 <label for="chkrstrip">%(label)s</label>
 ''' % {
-            'checked': ('', 'checked')[form.get('rstrip', ['0'])[0] == '1'],
+            'checked': ('', 'checked')[form.get('rstrip', '0') == '1'],
             'label': _('Remove trailing whitespace from each line')
             })
         request.write("</p>")
@@ -514,10 +512,10 @@
         self._save_draft(newtext, rev) # shall we really save a draft on CANCEL?
         self.lock.release()
 
-        backto = request.form.get('backto', [None])[0]
+        backto = request.values.get('backto')
         if backto:
             pg = Page(request, backto)
-            request.http_redirect(pg.url(request, relative=False))
+            request.http_redirect(pg.url(request))
         else:
             request.theme.add_msg(_('Edit was cancelled.'), "error")
             self.send_page()
@@ -672,7 +670,7 @@
 
             # Remove cache entry (if exists)
             pg = Page(self.request, self.page_name)
-            key = self.request.form.get('key', ['text_html'])[0]
+            key = self.request.form.get('key', 'text_html') # XXX see cleanup code in deletePage
             caching.CacheEntry(self.request, pg, key, scope='item').remove()
             caching.CacheEntry(self.request, pg, "pagelinks", scope='item').remove()
 
@@ -769,6 +767,7 @@
         signature = u.signature()
         variables = {
             'PAGE': self.page_name,
+            'TIMESTAMP': now,
             'TIME': "<<DateTime(%s)>>" % now,
             'DATE': "<<Date(%s)>>" % now,
             'ME': u.name,
@@ -784,8 +783,8 @@
             # Users can define their own variables via
             # UserHomepage/MyDict, which override the default variables.
             userDictPage = u.name + "/MyDict"
-            if request.dicts.has_dict(userDictPage):
-                variables.update(request.dicts.dict(userDictPage))
+            if userDictPage in request.dicts:
+                variables.update(request.dicts[userDictPage])
 
         for name in variables:
             text = text.replace('@%s@' % name, variables[name])
@@ -1122,7 +1121,6 @@
             trivial = kw.get('trivial', 0)
             # write the page file
             mtime_usecs, rev = self._write_file(newtext, action, comment, extra, deleted=deleted)
-            self.clean_acl_cache()
             self._save_draft(None, None) # everything fine, kill the draft for this page
 
             if notify:
--- a/MoinMoin/PageGraphicalEditor.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/PageGraphicalEditor.py	Mon Feb 08 21:49:19 2010 +0100
@@ -87,12 +87,11 @@
 
         # Emit http_headers after checks (send_page)
         request.disableHttpCaching(level=2)
-        request.emit_http_headers()
 
         # check if we want to load a draft
         use_draft = None
         if 'button_load_draft' in form:
-            wanted_draft_timestamp = int(form.get('draft_ts', ['0'])[0])
+            wanted_draft_timestamp = int(form.get('draft_ts', '0'))
             if wanted_draft_timestamp:
                 draft = self._load_draft()
                 if draft is not None:
@@ -104,7 +103,7 @@
         if use_draft is not None:
             title = _('Draft of "%(pagename)s"')
             # Propagate original revision
-            rev = int(form['draft_rev'][0])
+            rev = int(form['draft_rev'])
             self.set_raw_body(use_draft, modified=1)
             preview = use_draft
         elif preview is None:
@@ -124,7 +123,7 @@
 
         # get request parameters
         try:
-            text_rows = int(form['rows'][0])
+            text_rows = int(form['rows'])
         except StandardError:
             text_rows = self.cfg.edit_rows
             if request.user.valid:
@@ -168,9 +167,9 @@
             # If the page exists, we get the text from the page.
             # TODO: maybe warn if template argument was ignored because the page exists?
             raw_body = self.get_raw_body()
-        elif 'template' in form:
+        elif 'template' in request.values:
             # If the page does not exist, we try to get the content from the template parameter.
-            template_page = wikiutil.unquoteWikiname(form['template'][0])
+            template_page = wikiutil.unquoteWikiname(request.values['template'])
             if request.user.may.read(template_page):
                 raw_body = Page(request, template_page).get_raw_body()
                 if raw_body:
@@ -224,9 +223,8 @@
             raw_body = _('Describe %s here.') % (self.page_name, )
 
         # send form
-        request.write('<form id="editor" method="post" action="%s/%s#preview">' % (
-            request.getScriptname(),
-            wikiutil.quoteWikinameURL(self.page_name),
+        request.write('<form id="editor" method="post" action="%s#preview">' % (
+                request.href(self.page_name)
             ))
 
         # yet another weird workaround for broken IE6 (it expands the text
@@ -247,7 +245,7 @@
         request.write('<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket(request))
 
         # Save backto in a hidden input
-        backto = form.get('backto', [None])[0]
+        backto = request.values.get('backto')
         if backto:
             request.write(unicode(html.INPUT(type="hidden", name="backto", value=backto)))
 
@@ -298,7 +296,7 @@
 <input type="checkbox" name="trivial" id="chktrivialtop" value="1" %(checked)s onclick="toggle_trivial(this)">
 <label for="chktrivialtop">%(label)s</label>
 ''' % {
-          'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'],
+          'checked': ('', 'checked')[form.get('trivial', '0') == '1'],
           'label': _("Trivial change"),
        })
 
@@ -315,9 +313,7 @@
         url_prefix_local = request.cfg.url_prefix_local
         wikipage = wikiutil.quoteWikinameURL(self.page_name)
         fckbasepath = request.cfg.url_prefix_fckeditor
-        wikiurl = request.getScriptname()
-        if not wikiurl or wikiurl[-1] != '/':
-            wikiurl += '/'
+        wikiurl = request.script_root + '/'
         themepath = '%s/%s' % (url_prefix_static, request.theme.name)
         smileypath = themepath + '/img'
         # auto-generating a list for SmileyImages does NOT work from here!
@@ -377,7 +373,7 @@
 &nbsp;
 <input type="checkbox" name="trivial" id="chktrivial" value="1" %(checked)s onclick="toggle_trivial(this)">
 <label for="chktrivial">%(label)s</label> ''' % {
-                'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'],
+                'checked': ('', 'checked')[form.get('trivial', '0') == '1'],
                 'label': _("Trivial change"),
                 })
 
@@ -386,7 +382,7 @@
 <input type="checkbox" name="rstrip" id="chkrstrip" value="1" %(checked)s>
 <label for="chkrstrip">%(label)s</label>
 </p> ''' % {
-            'checked': ('', 'checked')[form.get('rstrip', ['0'])[0] == '1'],
+            'checked': ('', 'checked')[form.get('rstrip', '0') == '1'],
             'label': _('Remove trailing whitespace from each line')
             })
 
--- a/MoinMoin/_tests/__init__.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/__init__.py	Mon Feb 08 21:49:19 2010 +0100
@@ -15,6 +15,7 @@
 from MoinMoin.PageEditor import PageEditor
 from MoinMoin.util import random_string
 from MoinMoin import caching, user
+from MoinMoin.action import AttachFile
 
 # Promoting the test user -------------------------------------------
 # Usually the tests run as anonymous user, but for some stuff, you
@@ -93,6 +94,9 @@
 
 def nuke_page(request, pagename):
     """ completely delete a page, everything in the pagedir """
+    attachments = AttachFile._get_files(request, pagename)
+    for attachment in attachments:
+        AttachFile.remove_attachment(request, pagename, attachment)
     page = PageEditor(request, pagename, do_editor_backup=False)
     page.deletePage()
     # really get rid of everything there:
@@ -115,3 +119,9 @@
     p.form = request.form
     m = macro.Macro(p)
     return m
+
+def nuke_xapian_index(request):
+    """ completely delete everything in xapian index dir """
+    fpath = os.path.join(request.cfg.cache_dir, 'xapian')
+    if os.path.exists(fpath):
+        shutil.rmtree(fpath, True)
--- a/MoinMoin/_tests/_test_template.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/_test_template.py	Mon Feb 08 21:49:19 2010 +0100
@@ -6,6 +6,7 @@
 
     @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
                 2007 MoinMoin:AlexanderSchremmer
+                2009 MoinMoin:ReimarBauer
     @license: GNU GPL, see COPYING for details.
 """
 
@@ -13,7 +14,7 @@
 from MoinMoin import module_tested
 
 
-class TestSimpleStuff:
+class TestSimpleStuff(object):
     """ The simplest MoinMoin test class
 
     Class name must start with 'Test' to be included in
@@ -34,7 +35,7 @@
         assert result == expected
 
 
-class TestComplexStuff:
+class TestComplexStuff(object):
     """ Describe these tests here...
 
     Some tests may have a list of tests related to this test case. You
--- a/MoinMoin/_tests/ldap_testbase.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/ldap_testbase.py	Mon Feb 08 21:49:19 2010 +0100
@@ -32,8 +32,6 @@
 
     # /etc/init.d/apparmor stop
 
-    Requires Python 2.4 (for subprocess module).
-
     @copyright: 2008 by Thomas Waldmann
     @license: GNU GPL, see COPYING for details.
 """
@@ -41,14 +39,10 @@
 SLAPD_EXECUTABLE = 'slapd'  # filename of LDAP server executable - if it is not
                             # in your PATH, you have to give full path/filename.
 
-import os, shutil, tempfile, time
+import os, shutil, tempfile, time, base64, md5
 from StringIO import StringIO
 import signal
-
-try:
-    import subprocess  # needs Python 2.4
-except ImportError:
-    subprocess = None
+import subprocess
 
 try:
     import ldap, ldif, ldap.modlist  # needs python-ldap
@@ -61,8 +55,6 @@
         Either return some failure reason if we can't or None if everything
         looks OK.
     """
-    if subprocess is None:
-        return "You need at least python 2.4 to use ldap_testbase."
     if ldap is None:
         return "You need python-ldap installed to use ldap_testbase."
     slapd = False
@@ -187,6 +179,8 @@
         f.write(db_config)
         f.close()
 
+        rootpw = '{MD5}' + base64.b64encode(md5.new(self.rootpw).digest())
+
         # create slapd.conf from content template in slapd_config
         slapd_config = slapd_config % {
             'ldap_dir': self.ldap_dir,
@@ -194,7 +188,7 @@
             'schema_dir': self.schema_dir,
             'basedn': self.basedn,
             'rootdn': self.rootdn,
-            'rootpw': self.rootpw,
+            'rootpw': rootpw,
         }
         if isinstance(slapd_config, unicode):
             slapd_config = slapd_config.encode(self.coding)
--- a/MoinMoin/_tests/ldap_testdata.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/ldap_testdata.py	Mon Feb 08 21:49:19 2010 +0100
@@ -78,14 +78,16 @@
 objectClass: account
 objectClass: simpleSecurityObject
 uid: usera
-userPassword: usera
+# this is md5 encoded 'usera' for password
+userPassword: {MD5}aXqgOSc5gSW7YoLi9BSmvg==
 
 dn: uid=userb,ou=Unit B,ou=Users,ou=testing,dc=example,dc=org
 cn: Vorname Nachname
 objectClass: inetOrgPerson
 sn: Nachname
 uid: userb
-userPassword: userb
+# this is md5 encoded 'userb' for password
+userPassword: {MD5}ThvfQsM7OQFjqSUQOX2XsA==
 
 dn: cn=Group A,ou=Groups,ou=testing,dc=example,dc=org
 cn: Group A
--- a/MoinMoin/_tests/test_PageEditor.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/test_PageEditor.py	Mon Feb 08 21:49:19 2010 +0100
@@ -147,13 +147,7 @@
 
     def deleteCaches(self):
         """ Force the wiki to scan the test page into the dicts """
-        from MoinMoin import caching
-        caching.CacheEntry(self.request, 'wikidicts', 'dicts_groups', scope='wiki').remove()
-        if hasattr(self.request, 'dicts'):
-            del self.request.dicts
-        if hasattr(self.request.cfg, 'DICTS_DATA'):
-            del self.request.cfg.DICTS_DATA
-        self.request.pages = {}
+        # New dicts does not require cache refresh.
 
     def deleteTestPage(self):
         """ Delete temporary page, bypass logs and notifications """
@@ -174,6 +168,7 @@
 
     def teardown_method(self, method):
         self.request.cfg.event_handlers = self.old_handlers
+        nuke_page(self.request, u'AutoCreatedMoinMoinTemporaryTestPageFortestSave')
 
     def testSaveAbort(self):
         """Test if saveText() is interrupted if PagePreSave event handler returns Abort"""
--- a/MoinMoin/_tests/test_packages.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/test_packages.py	Mon Feb 08 21:49:19 2010 +0100
@@ -18,7 +18,7 @@
 from MoinMoin.action import AttachFile
 from MoinMoin.action.PackagePages import PackagePages
 from MoinMoin.packages import Package, ScriptEngine, MOIN_PACKAGE_FILE, ZipPackage, packLine, unpackLine
-from MoinMoin._tests import become_superuser, create_page, nuke_page
+from MoinMoin._tests import become_trusted, become_superuser, create_page, nuke_page
 from MoinMoin.Page import Page
 from MoinMoin.PageEditor import PageEditor
 
@@ -86,7 +86,7 @@
 
     def testSearch(self):
         package = PackagePages(self.request.rootpage.page_name, self.request)
-        assert package.searchpackage(self.request, "BadCon") == [u'BadContent']
+        assert package.searchpackage(self.request, "title:BadCon") == [u'BadContent']
 
     def testListCreate(self):
         package = PackagePages(self.request.rootpage.page_name, self.request)
@@ -132,6 +132,7 @@
         return zip_file
 
     def testAttachments_after_page_creation(self):
+        become_trusted(self.request)
         pagename = u'PackageTestPageCreatedFirst'
         page = create_page(self.request, pagename, u"This page has not yet an attachments dir")
         script = u"""MoinMoinPackage|1
@@ -149,6 +150,7 @@
         os.unlink(zip_file)
 
     def testAttachments_without_page_creation(self):
+        become_trusted(self.request)
         pagename = u"PackageAttachmentAttachWithoutPageCreation"
         script = u"""MoinMoinPackage|1
 AddAttachment|1_attachment|my_test.txt|%(pagename)s
--- a/MoinMoin/_tests/test_sourcecode.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/test_sourcecode.py	Mon Feb 08 21:49:19 2010 +0100
@@ -19,10 +19,10 @@
 EXCLUDE = [
     '/contrib/DesktopEdition/setup_py2exe.py', # has crlf
     '/contrib/TWikiDrawPlugin', # 3rd party java stuff
+    '/contrib/flup-server', # 3rd party WSGI adapters
     '/MoinMoin/support', # 3rd party libs or non-broken stdlib stuff
-    '/wiki/htdocs/applets/FCKeditor', # 3rd party GUI editor
+    '/MoinMoin/web/static/htdocs', # this is our dist static stuff
     '/tests/wiki', # this is our test wiki
-    '/wiki/htdocs', # this is our dist static stuff
     '/wiki/data/pages', # wiki pages, there may be .py attachments
 ]
 
--- a/MoinMoin/_tests/test_user.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/test_user.py	Mon Feb 08 21:49:19 2010 +0100
@@ -39,11 +39,11 @@
 
     def setup_method(self, method):
         # Save original user and cookie
-        self.saved_cookie = self.request.saved_cookie
+        self.saved_cookie = self.request.cookies
         self.saved_user = self.request.user
 
         # Create anon user for the tests
-        self.request.saved_cookie = ''
+        self.request.cookies = {}
         self.request.user = user.User(self.request)
 
         # Prevent user list caching - we create and delete users too fast for that.
@@ -65,7 +65,7 @@
             del self.user
 
         # Restore original user
-        self.request.saved_cookie = self.saved_cookie
+        self.request.cookies = self.saved_cookie
         self.request.user = self.saved_user
 
         # Remove user name to id cache, or next test will fail
--- a/MoinMoin/_tests/test_wikidicts.py	Mon Feb 08 19:01:03 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,231 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-"""
-    MoinMoin - MoinMoin.wikidicts tests
-
-    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
-                2007 by MoinMoin:ThomasWaldmann
-    @license: GNU GPL, see COPYING for details.
-"""
-
-import py
-import re
-import shutil
-
-from MoinMoin import wikidicts
-from MoinMoin import Page
-from MoinMoin.PageEditor import PageEditor
-from MoinMoin.user import User
-from MoinMoin._tests import append_page, become_trusted, create_page, create_random_string_list, nuke_page, nuke_user
-
-class TestGroupPage:
-
-    def testCamelCase(self):
-        """ wikidicts: initFromText: CamelCase links """
-        text = """
- * CamelCase
-"""
-        assert self.getMembers(text) == ['CamelCase']
-
-    def testExtendedName(self):
-        """ wikidicts: initFromText: extended names """
-        text = """
- * extended name
-"""
-        assert self.getMembers(text) == ['extended name']
-
-    def testExtendedLink(self):
-        """ wikidicts: initFromText: extended link """
-        text = """
- * [[extended link]]
-"""
-        assert self.getMembers(text) == ['extended link']
-
-    def testIgnoreSecondLevelList(self):
-        """ wikidicts: initFromText: ignore non first level items """
-        text = """
-  * second level
-   * third level
-    * forth level
-     * and then some...
-"""
-        assert self.getMembers(text) == []
-
-    def testIgnoreOther(self):
-        """ wikidicts: initFromText: ignore anything but first level list itmes """
-        text = """
-= ignore this =
- * take this
-
-Ignore previous line and this text.
-"""
-        assert self.getMembers(text) == ['take this']
-
-    def testStripWhitespace(self):
-        """ wikidicts: initFromText: strip whitespace around items """
-        text = """
- *   take this
-"""
-        assert self.getMembers(text) == ['take this']
-
-    def getMembers(self, text):
-        group = wikidicts.Group(self.request, '')
-        group.initFromText(text)
-        return group.members()
-
-
-class TestDictPage:
-
-    def testGroupMembers(self):
-        """ wikidicts: create dict from keys and values in text """
-        text = '''
-Text ignored
- * list items ignored
-  * Second level list ignored
- First:: first item
- text with spaces:: second item
-
-Empty lines ignored, so is this text
-Next line has key with empty value
- Empty string::\x20
- Last:: last item
-'''
-        d = wikidicts.Dict(self.request, '')
-        d.initFromText(text)
-        assert d['First'] == 'first item'
-        assert d['text with spaces'] == 'second item'
-        assert d['Empty string'] == '' # XXX fails if trailing blank is missing
-        assert d['Last'] == 'last item'
-        assert len(d) == 4
-
-class TestGroupDicts:
-
-    def testSystemPagesGroupInDicts(self):
-        """ wikidict: names in SystemPagesGroup should be in request.dicts
-
-        Get a list of all pages, and check that the dicts list all of them.
-
-        Assume that the SystemPagesGroup is in the data or the underlay dir.
-        """
-        assert Page.Page(self.request, 'SystemPagesGroup').exists(), "SystemPagesGroup is missing, Can't run test"
-        systemPages = wikidicts.Group(self.request, 'SystemPagesGroup')
-        #print repr(systemPages)
-        #print repr(self.request.dicts['SystemPagesGroup'])
-        for member in systemPages.members():
-            assert self.request.dicts.has_member('SystemPagesGroup', member), '%s should be in request.dict' % member
-
-        members, groups = self.request.dicts.expand_group('SystemPagesGroup')
-        assert 'SystemPagesInEnglishGroup' in groups
-        assert 'RecentChanges' in members
-        assert 'HelpContents' in members
-
-    def testRenameGroupPage(self):
-        """
-         tests if the dict cache for groups is refreshed after renaming a Group page
-        """
-        request = self.request
-        become_trusted(request)
-        page = create_page(request, u'SomeGroup', u" * ExampleUser")
-        page.renamePage('AnotherGroup')
-        group = wikidicts.Group(request, '')
-        isgroup = request.cfg.cache.page_group_regexact.search
-        grouppages = request.rootpage.getPageList(user='', filter=isgroup)
-        result = request.dicts.has_member(u'AnotherGroup', u'ExampleUser')
-        nuke_page(request, u'AnotherGroup')
-
-        assert result is True
-
-    def testCopyGroupPage(self):
-        """
-         tests if the dict cache for groups is refreshed after copying a Group page
-        """
-        request = self.request
-        become_trusted(request)
-        page = create_page(request, u'SomeGroup', u" * ExampleUser")
-        page.copyPage(u'OtherGroup')
-        group = wikidicts.Group(request, '')
-        isgroup = request.cfg.cache.page_group_regexact.search
-        grouppages = request.rootpage.getPageList(user='', filter=isgroup)
-        result = request.dicts.has_member(u'OtherGroup', u'ExampleUser')
-        nuke_page(request, u'OtherGroup')
-        nuke_page(request, u'SomeGroup')
-
-        assert result is True
-
-    def testAppendingGroupPage(self):
-        """
-         tests scalability by appending a name to a large list of group members
-        """
-        # long list of users
-        page_content = [u" * %s" % member for member in create_random_string_list(length=15, count=30000)]
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        page = create_page(request, u'UserGroup', "\n".join(page_content))
-        page = append_page(request, u'UserGroup', u' * %s' % test_user)
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-
-        assert result is True
-
-    def testUserAppendingGroupPage(self):
-        """
-         tests appending a username to a large list of group members and user creation
-        """
-        # long list of users
-        page_content = [u" * %s" % member for member in create_random_string_list()]
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        page = create_page(request, u'UserGroup', "\n".join(page_content))
-        page = append_page(request, u'UserGroup', u' * %s' % test_user)
-
-        # now shortly later we create a user object
-        user = User(request, name=test_user)
-        if not user.exists():
-            User(request, name=test_user, password=test_user).save()
-
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-        nuke_user(request, test_user)
-
-        assert result is True
-
-    def testMemberRemovedFromGroupPage(self):
-        """
-         tests appending a member to a large list of group members and recreating the page without the member
-        """
-        # long list of users
-        page_content = [u" * %s" % member for member in create_random_string_list()]
-        page_content = "\n".join(page_content)
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        page = create_page(request, u'UserGroup', page_content)
-        page = append_page(request, u'UserGroup', u' * %s' % test_user)
-        # saves the text without test_user
-        page.saveText(page_content, 0)
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-
-        assert result is False
-
-    def testGroupPageTrivialChange(self):
-        """
-         tests appending a username to a group page by trivial change
-        """
-        request = self.request
-        become_trusted(request)
-        test_user = create_random_string_list(length=15, count=1)[0]
-        member = u" * %s\n" % test_user
-        page = create_page(request, u'UserGroup', member)
-        # next member saved  as trivial change
-        test_user = create_random_string_list(length=15, count=1)[0]
-        member = u" * %s\n" % test_user
-        page.saveText(member, 0, trivial=1)
-        result = request.dicts.has_member('UserGroup', test_user)
-        nuke_page(request, u'UserGroup')
-
-        assert result is True
-
-coverage_modules = ['MoinMoin.wikidicts']
-
--- a/MoinMoin/_tests/test_wikiutil.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/_tests/test_wikiutil.py	Mon Feb 08 21:49:19 2010 +0100
@@ -9,7 +9,9 @@
 
 import py
 
-from MoinMoin import wikiutil
+from MoinMoin import config, wikiutil
+
+from werkzeug import MultiDict
 
 
 class TestQueryStringSupport:
@@ -21,17 +23,13 @@
     ]
     def testParseQueryString(self):
         for qstr, expected_str, expected_unicode in self.tests:
-            assert wikiutil.parseQueryString(qstr, want_unicode=False) == expected_str
-            assert wikiutil.parseQueryString(qstr, want_unicode=True) == expected_unicode
-            assert wikiutil.parseQueryString(unicode(qstr), want_unicode=False) == expected_str
-            assert wikiutil.parseQueryString(unicode(qstr), want_unicode=True) == expected_unicode
+            assert wikiutil.parseQueryString(qstr) == MultiDict(expected_unicode)
+            assert wikiutil.parseQueryString(unicode(qstr)) == MultiDict(expected_unicode)
 
     def testMakeQueryString(self):
         for qstr, in_str, in_unicode in self.tests:
-            assert wikiutil.parseQueryString(wikiutil.makeQueryString(in_unicode, want_unicode=False), want_unicode=False) == in_str
-            assert wikiutil.parseQueryString(wikiutil.makeQueryString(in_str, want_unicode=False), want_unicode=False) == in_str
-            assert wikiutil.parseQueryString(wikiutil.makeQueryString(in_unicode, want_unicode=True), want_unicode=True) == in_unicode
-            assert wikiutil.parseQueryString(wikiutil.makeQueryString(in_str, want_unicode=True), want_unicode=True) == in_unicode
+            assert wikiutil.parseQueryString(wikiutil.makeQueryString(in_unicode)) == MultiDict(in_unicode)
+            assert wikiutil.parseQueryString(wikiutil.makeQueryString(in_str)) == MultiDict(in_unicode)
 
 
 class TestTickets:
@@ -87,15 +85,8 @@
             assert wikiutil.join_wiki(baseurl, pagename) == url
 
 
-class TestSystemPagesGroup:
-    def testSystemPagesGroupNotEmpty(self):
-        assert self.request.dicts.members('SystemPagesGroup')
-
 class TestSystemPage:
     systemPages = (
-        # First level, on SystemPagesGroup
-        'SystemPagesInEnglishGroup',
-        # Second level, on one of the pages above
         'RecentChanges',
         'TitleIndex',
         )
@@ -973,4 +964,127 @@
         assert relative_page == wikiutil.RelPageName(current_page, absolute_page)
 
 
+class TestNormalizePagename(object):
+
+    def testPageInvalidChars(self):
+        """ request: normalize pagename: remove invalid unicode chars
+
+        Assume the default setting
+        """
+        test = u'\u0000\u202a\u202b\u202c\u202d\u202e'
+        expected = u''
+        result = wikiutil.normalize_pagename(test, self.request.cfg)
+        assert result == expected
+
+    def testNormalizeSlashes(self):
+        """ request: normalize pagename: normalize slashes """
+        cases = (
+            (u'/////', u''),
+            (u'/a', u'a'),
+            (u'a/', u'a'),
+            (u'a/////b/////c', u'a/b/c'),
+            (u'a b/////c d/////e f', u'a b/c d/e f'),
+            )
+        for test, expected in cases:
+            result = wikiutil.normalize_pagename(test, self.request.cfg)
+            assert result == expected
+
+    def testNormalizeWhitespace(self):
+        """ request: normalize pagename: normalize whitespace """
+        cases = (
+            (u'         ', u''),
+            (u'    a', u'a'),
+            (u'a    ', u'a'),
+            (u'a     b     c', u'a b c'),
+            (u'a   b  /  c    d  /  e   f', u'a b/c d/e f'),
+            # All 30 unicode spaces
+            (config.chars_spaces, u''),
+            )
+        for test, expected in cases:
+            result = wikiutil.normalize_pagename(test, self.request.cfg)
+            assert result == expected
+
+    def testUnderscoreTestCase(self):
+        """ request: normalize pagename: underscore convert to spaces and normalized
+
+        Underscores should convert to spaces, then spaces should be
+        normalized, order is important!
+        """
+        cases = (
+            (u'         ', u''),
+            (u'  a', u'a'),
+            (u'a  ', u'a'),
+            (u'a  b  c', u'a b c'),
+            (u'a  b  /  c  d  /  e  f', u'a b/c d/e f'),
+            )
+        for test, expected in cases:
+            result = wikiutil.normalize_pagename(test, self.request.cfg)
+            assert result == expected
+
+class TestGroupPages(object):
+
+    def testNormalizeGroupName(self):
+        """ request: normalize pagename: restrict groups to alpha numeric Unicode
+
+        Spaces should normalize after invalid chars removed!
+        """
+        cases = (
+            # current acl chars
+            (u'Name,:Group', u'NameGroup'),
+            # remove than normalize spaces
+            (u'Name ! @ # $ % ^ & * ( ) + Group', u'Name Group'),
+            )
+        for test, expected in cases:
+            # validate we are testing valid group names
+            if wikiutil.isGroupPage(test, self.request.cfg):
+                result = wikiutil.normalize_pagename(test, self.request.cfg)
+                assert result == expected
+
+class TestVersion(object):
+    def test_Version(self):
+        Version = wikiutil.Version
+        # test properties
+        assert Version(1, 2, 3).major == 1
+        assert Version(1, 2, 3).minor == 2
+        assert Version(1, 2, 3).release == 3
+        assert Version(1, 2, 3, '4.5alpha6').additional == '4.5alpha6'
+        # test Version init and Version to str conversion
+        assert str(Version(1)) == "1.0.0"
+        assert str(Version(1, 2)) == "1.2.0"
+        assert str(Version(1, 2, 3)) == "1.2.3"
+        assert str(Version(1, 2, 3, '4.5alpha6')) == "1.2.3-4.5alpha6"
+        assert str(Version(version='1.2.3')) == "1.2.3"
+        assert str(Version(version='1.2.3-4.5alpha6')) == "1.2.3-4.5alpha6"
+        # test Version comparison, trivial cases
+        assert Version() == Version()
+        assert Version(1) == Version(1)
+        assert Version(1, 2) == Version(1, 2)
+        assert Version(1, 2, 3) == Version(1, 2, 3)
+        assert Version(1, 2, 3, 'foo') == Version(1, 2, 3, 'foo')
+        assert Version(1) != Version(2)
+        assert Version(1, 2) != Version(1, 3)
+        assert Version(1, 2, 3) != Version(1, 2, 4)
+        assert Version(1, 2, 3, 'foo') != Version(1, 2, 3, 'bar')
+        assert Version(1) < Version(2)
+        assert Version(1, 2) < Version(1, 3)
+        assert Version(1, 2, 3) < Version(1, 2, 4)
+        assert Version(1, 2, 3, 'bar') < Version(1, 2, 3, 'foo')
+        assert Version(2) > Version(1)
+        assert Version(1, 3) > Version(1, 2)
+        assert Version(1, 2, 4) > Version(1, 2, 3)
+        assert Version(1, 2, 3, 'foo') > Version(1, 2, 3, 'bar')
+        # test Version comparison, more delicate cases
+        assert Version(1, 12) > Version(1, 9)
+        assert Version(1, 12) > Version(1, 1, 2)
+        assert Version(1, 0, 0, '0.0a2') > Version(1, 0, 0, '0.0a1')
+        assert Version(1, 0, 0, '0.0b1') > Version(1, 0, 0, '0.0a9')
+        assert Version(1, 0, 0, '0.0b2') > Version(1, 0, 0, '0.0b1')
+        assert Version(1, 0, 0, '0.0c1') > Version(1, 0, 0, '0.0b9')
+        assert Version(1, 0, 0, '1') > Version(1, 0, 0, '0.0c9')
+        # test Version playing nice with tuples
+        assert Version(1, 2, 3) == (1, 2, 3, '')
+        assert Version(1, 2, 4) > (1, 2, 3)
+
+
 coverage_modules = ['MoinMoin.wikiutil']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_wsgiapp.py	Mon Feb 08 21:49:19 2010 +0100
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - MoinMoin.wsgiapp Tests
+
+    @copyright: 2008 MoinMoin:FlorianKrupicka
+    @license: GNU GPL, see COPYING for details.
+"""
+from StringIO import StringIO
+
+from MoinMoin import wsgiapp
+
+DOC_TYPE = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
+
+class TestApplication:
+    # self.client is made by conftest
+
+    # These should exist
+    PAGES = ('FrontPage', 'RecentChanges', 'HelpContents', 'FindPage')
+    # ... and these should not
+    NO_PAGES = ('FooBar', 'TheNone/ExistantPage/', '%33Strange%74Codes')
+
+    def testWSGIAppExisting(self):
+        for page in self.PAGES:
+            def _test_(page=page):
+                appiter, status, headers = self.client.get('/%s' % page)
+                output = ''.join(appiter)
+                print output
+                assert status[:3] == '200'
+                assert ('Content-Type', 'text/html; charset=utf-8') in headers
+                for needle in (DOC_TYPE, page):
+                    assert needle in output
+            yield _test_
+
+    def testWSGIAppAbsent(self):
+        for page in self.NO_PAGES:
+            def _test_(page=page):
+                appiter, status, headers = self.client.get('/%s' % page)
+                assert status[:3] == '404'
+                output = ''.join(appiter)
+                for needle in ('new empty page', 'page template'):
+                    assert needle in output
+            yield _test_
--- a/MoinMoin/action/AttachFile.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/AttachFile.py	Mon Feb 08 21:49:19 2010 +0100
@@ -27,16 +27,24 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import os, time, zipfile, mimetypes, errno
+import os, time, zipfile, errno, datetime
+from StringIO import StringIO
+
+from werkzeug import http_date
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
 
-from MoinMoin import config, wikiutil, packages
+# keep both imports below as they are, order is important:
+from MoinMoin import wikiutil
+import mimetypes
+
+from MoinMoin import config, 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
+from MoinMoin.events import FileAttachedEvent, FileRemovedEvent, send_event
+from MoinMoin.support import tarfile
 
 action_name = __name__.split('.')[-1]
 
@@ -77,39 +85,44 @@
         return u"/".join(pieces[:-1]), pieces[-1]
 
 
-def attachUrl(request, pagename, filename=None, **kw):
-    # filename is not used yet, but should be used later to make a sub-item url
-    if not (kw.get('do') in ['get', 'view', None]
-            and
-            kw.get('rename') is None):
-        # create a ticket for the not so harmless operations
-        kw['ticket'] = wikiutil.createTicket(request)
-    if kw:
-        qs = '?%s' % wikiutil.makeQueryString(kw, want_unicode=False)
-    else:
-        qs = ''
-    return "%s/%s%s" % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), qs)
+def get_action(request, filename, do):
+    generic_do_mapping = {
+        # do -> action
+        'get': action_name,
+        'view': action_name,
+        'move': action_name,
+        'del': action_name,
+        'unzip': action_name,
+        'install': action_name,
+        'upload_form': action_name,
+    }
+    basename, ext = os.path.splitext(filename)
+    do_mapping = request.cfg.extensions_mapping.get(ext, {})
+    action = do_mapping.get(do, None)
+    if action is None:
+        # we have no special support for this,
+        # look up whether we have generic support:
+        action = generic_do_mapping.get(do, None)
+    return action
 
 
-def getAttachUrl(pagename, filename, request, addts=0, escaped=0, do='get', drawing='', upload=False):
-    """ Get URL that points to attachment `filename` of page `pagename`. """
-    if upload:
-        if not drawing:
-            url = attachUrl(request, pagename, filename,
-                            rename=wikiutil.taintfilename(filename), action=action_name)
-        else:
-            url = attachUrl(request, pagename, filename,
-                            rename=wikiutil.taintfilename(filename), drawing=drawing, action=action_name)
-    else:
-        if not drawing:
-            url = attachUrl(request, pagename, filename,
-                            target=filename, action=action_name, do=do)
-        else:
-            url = attachUrl(request, pagename, filename,
-                            drawing=drawing, action=action_name)
-    if escaped:
-        url = wikiutil.escape(url)
-    return url
+def getAttachUrl(pagename, filename, request, addts=0, do='get'):
+    """ Get URL that points to attachment `filename` of page `pagename`.
+        For upload url, call with do='upload_form'.
+        Returns the URL to do the specified "do" action or None,
+        if this action is not supported.
+    """
+    action = get_action(request, filename, do)
+    if action:
+        args = dict(action=action, do=do, target=filename)
+        if do not in ['get', 'view', # harmless
+                      'modify', # just renders the applet html, which has own ticket
+                      'move', # renders rename form, which has own ticket
+            ]:
+            # create a ticket for the not so harmless operations
+            args['ticket'] = wikiutil.createTicket(request)
+        url = request.href(pagename, **args)
+        return url
 
 
 def getIndicator(request, pagename):
@@ -128,7 +141,7 @@
     fmt = request.formatter
     attach_count = _('[%d attachments]') % len(files)
     attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
-    attach_link = (fmt.url(1, attachUrl(request, pagename, action=action_name), rel='nofollow') +
+    attach_link = (fmt.url(1, request.href(pagename, action=action_name), rel='nofollow') +
                    attach_icon +
                    fmt.url(0))
     return attach_link
@@ -192,34 +205,56 @@
         filecontent can be either a str (in memory file content),
         or an open file object (file content in e.g. a tempfile).
     """
-    _ = request.getText
-
     # replace illegal chars
     target = wikiutil.taintfilename(target)
 
     # get directory, and possibly create it
     attach_dir = getAttachDir(request, pagename, create=1)
-    # save file
     fpath = os.path.join(attach_dir, target).encode(config.charset)
+
     exists = os.path.exists(fpath)
-    if exists and not overwrite:
-        raise AttachmentAlreadyExists
+    if exists:
+        if overwrite:
+            remove_attachment(request, pagename, target)
+        else:
+            raise AttachmentAlreadyExists
+
+    # save file
+    stream = open(fpath, 'wb')
+    try:
+        _write_stream(filecontent, stream)
+    finally:
+        stream.close()
+
+    _addLogEntry(request, 'ATTNEW', pagename, target)
+
+    filesize = os.path.getsize(fpath)
+    event = FileAttachedEvent(request, pagename, target, filesize)
+    send_event(event)
+
+    return target, filesize
+
+
+def remove_attachment(request, pagename, target):
+    """ remove attachment <target> of page <pagename>
+    """
+    # replace illegal chars
+    target = wikiutil.taintfilename(target)
+
+    # get directory, do not create it
+    attach_dir = getAttachDir(request, pagename, create=0)
+    # remove file
+    fpath = os.path.join(attach_dir, target).encode(config.charset)
+    try:
+        filesize = os.path.getsize(fpath)
+        os.remove(fpath)
+    except:
+        # either it is gone already or we have no rights - not much we can do about it
+        filesize = 0
     else:
-        if exists:
-            try:
-                os.remove(fpath)
-            except:
-                pass
-        stream = open(fpath, 'wb')
-        try:
-            _write_stream(filecontent, stream)
-        finally:
-            stream.close()
+        _addLogEntry(request, 'ATTDEL', pagename, target)
 
-        _addLogEntry(request, 'ATTNEW', pagename, target)
-
-        filesize = os.path.getsize(fpath)
-        event = FileAttachedEvent(request, pagename, target, filesize)
+        event = FileRemovedEvent(request, pagename, target, filesize)
         send_event(event)
 
     return target, filesize
@@ -236,7 +271,7 @@
     """
     from MoinMoin.logfile import editlog
     t = wikiutil.timestamp2version(time.time())
-    fname = wikiutil.url_quote(filename, want_unicode=True)
+    fname = wikiutil.url_quote(filename)
 
     # Write to global log
     log = editlog.EditLog(request)
@@ -256,10 +291,10 @@
     _ = request.getText
 
     error = None
-    if not request.form.get('target', [''])[0]:
+    if not request.values.get('target'):
         error = _("Filename of attachment not specified!")
     else:
-        filename = wikiutil.taintfilename(request.form['target'][0])
+        filename = wikiutil.taintfilename(request.values['target'])
         fpath = getFilename(request, pagename, filename)
 
         if os.path.isfile(fpath):
@@ -300,6 +335,10 @@
         label_unzip = _("unzip")
         label_install = _("install")
 
+        may_read = request.user.may.read(pagename)
+        may_write = request.user.may.write(pagename)
+        may_delete = request.user.may.delete(pagename)
+
         html.append(fmt.bullet_list(1))
         for file in files:
             mt = wikiutil.MimeType(filename=file)
@@ -312,7 +351,6 @@
                        }
 
             links = []
-            may_delete = request.user.may.delete(pagename)
             if may_delete and not readonly:
                 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
                              fmt.text(label_del) +
@@ -327,14 +365,16 @@
                          fmt.text(label_get) +
                          fmt.url(0))
 
-            if ext == '.draw':
-                links.append(fmt.url(1, getAttachUrl(pagename, file, request, drawing=base)) +
-                             fmt.text(label_edit) +
-                             fmt.url(0))
-            else:
-                links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
-                             fmt.text(label_view) +
-                             fmt.url(0))
+            links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
+                         fmt.text(label_view) +
+                         fmt.url(0))
+
+            if may_write and not readonly:
+                edit_url = getAttachUrl(pagename, file, request, do='modify')
+                if edit_url:
+                    links.append(fmt.url(1, edit_url) +
+                                 fmt.text(label_edit) +
+                                 fmt.url(0))
 
             try:
                 is_zipfile = zipfile.is_zipfile(fullpath)
@@ -345,9 +385,7 @@
                                      fmt.text(label_install) +
                                      fmt.url(0))
                     elif (not is_package and mt.minor == 'zip' and
-                          may_delete and
-                          request.user.may.read(pagename) and
-                          request.user.may.write(pagename)):
+                          may_read and may_write and may_delete):
                         links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
                                      fmt.text(label_unzip) +
                                      fmt.url(0))
@@ -404,49 +442,10 @@
 def send_link_rel(request, pagename):
     files = _get_files(request, pagename)
     for fname in files:
-        url = getAttachUrl(pagename, fname, request, do='view', escaped=1)
+        url = getAttachUrl(pagename, fname, request, do='view')
         request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
-                      wikiutil.escape(fname, 1), url))
-
-
-def send_hotdraw(pagename, request):
-    _ = request.getText
-
-    now = time.time()
-    pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin"
-    basename = request.form['drawing'][0]
-    drawpath = getAttachUrl(pagename, basename + '.draw', request, escaped=1)
-    pngpath = getAttachUrl(pagename, basename + '.png', request, escaped=1)
-    pagelink = attachUrl(request, pagename, '', action=action_name, ts=now)
-    helplink = Page(request, "HelpOnActions/AttachFile").url(request)
-    savelink = attachUrl(request, pagename, '', action=action_name, do='savedrawing')
-    #savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted
-                                           # request, {'savename': request.form['drawing'][0]+'.draw'}
-    #savelink = '/cgi-bin/dumpform.bat'
-
-    timestamp = '&amp;ts=%s' % now
-
-    request.write('<h2>' + _("Edit drawing") + '</h2>')
-    request.write("""
-<p>
-<img src="%(pngpath)s%(timestamp)s">
-<applet code="CH.ifa.draw.twiki.TWikiDraw.class"
-        archive="%(pubpath)s/twikidraw.jar" width="640" height="480">
-<param name="drawpath" value="%(drawpath)s">
-<param name="pngpath"  value="%(pngpath)s">
-<param name="savepath" value="%(savelink)s">
-<param name="basename" value="%(basename)s">
-<param name="viewpath" value="%(pagelink)s">
-<param name="helppath" value="%(helplink)s">
-<strong>NOTE:</strong> You need a Java enabled browser to edit the drawing example.
-</applet>
-</p>""" % {
-    'pngpath': pngpath, 'timestamp': timestamp,
-    'pubpath': pubpath, 'drawpath': drawpath,
-    'savelink': savelink, 'pagelink': pagelink, 'helplink': helplink,
-    'basename': wikiutil.escape(basename, 1),
-})
-
+                      wikiutil.escape(fname, 1),
+                      wikiutil.escape(url, 1)))
 
 def send_uploadform(pagename, request):
     """ Send the HTML code for the list of already stored attachments and
@@ -466,12 +465,12 @@
     if writeable:
         request.write('<h2>' + _("New Attachment") + '</h2>')
         request.write("""
-<form action="%(baseurl)s/%(pagename)s" method="POST" enctype="multipart/form-data">
+<form action="%(url)s" method="POST" enctype="multipart/form-data">
 <dl>
 <dt>%(upload_label_file)s</dt>
 <dd><input type="file" name="file" size="50"></dd>
-<dt>%(upload_label_rename)s</dt>
-<dd><input type="text" name="rename" size="50" value="%(rename)s"></dd>
+<dt>%(upload_label_target)s</dt>
+<dd><input type="text" name="target" size="50" value="%(target)s"></dd>
 <dt>%(upload_label_overwrite)s</dt>
 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
 </dl>
@@ -484,14 +483,13 @@
 </p>
 </form>
 """ % {
-    'baseurl': request.getScriptname(),
-    'pagename': wikiutil.quoteWikinameURL(pagename),
+    'url': request.href(pagename),
     'action_name': action_name,
     'upload_label_file': _('File to upload'),
-    'upload_label_rename': _('Rename to'),
-    'rename': wikiutil.escape(request.form.get('rename', [''])[0], 1),
+    'upload_label_target': _('Rename to'),
+    'target': wikiutil.escape(request.values.get('target', ''), 1),
     'upload_label_overwrite': _('Overwrite existing attachment of same name'),
-    'overwrite_checked': ('', 'checked')[request.form.get('overwrite', ['0'])[0] == '1'],
+    'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
     'upload_button': _('Upload'),
     'textcha': TextCha(request).render(),
     'ticket': wikiutil.createTicket(request),
@@ -503,10 +501,6 @@
     if not writeable:
         request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
 
-    if writeable and request.form.get('drawing', [None])[0]:
-        send_hotdraw(pagename, request)
-
-
 #############################################################################
 ### Web interface for file upload, viewing and deletion
 #############################################################################
@@ -515,12 +509,12 @@
     """ Main dispatcher for the 'AttachFile' action. """
     _ = request.getText
 
-    do = request.form.get('do', ['upload_form'])
-    handler = globals().get('_do_%s' % do[0])
+    do = request.values.get('do', 'upload_form')
+    handler = globals().get('_do_%s' % do)
     if handler:
         msg = handler(pagename, request)
     else:
-        msg = _('Unsupported AttachFile sub-action: %s') % do[0]
+        msg = _('Unsupported AttachFile sub-action: %s') % do
     if msg:
         error_msg(pagename, request, msg)
 
@@ -534,7 +528,6 @@
         msg = wikiutil.escape(msg)
     _ = request.getText
 
-    request.emit_http_headers()
     # Use user interface language for this generated page
     request.setContentLanguage(request.lang)
     request.theme.add_msg(msg, "dialog")
@@ -546,21 +539,10 @@
     request.theme.send_closing_html()
 
 
-def preprocess_filename(filename):
-    """ preprocess the filename we got from upload form,
-        strip leading drive and path (IE misbehaviour)
-    """
-    if filename and len(filename) > 1 and (filename[1] == ':' or filename[0] == '\\'): # C:.... or \path... or \\server\...
-        bsindex = filename.rfind('\\')
-        if bsindex >= 0:
-            filename = filename[bsindex+1:]
-    return filename
-
-
 def _do_upload(pagename, request):
     _ = request.getText
 
-    if not wikiutil.checkTicket(request, request.form.get('ticket', [''])[0]):
+    if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.upload' }
 
     # Currently we only check TextCha for upload (this is what spammers ususally do),
@@ -569,9 +551,15 @@
         return _('TextCha: Wrong answer! Go back and try again...')
 
     form = request.form
-    overwrite = form.get('overwrite', [u'0'])[0]
+
+    file_upload = request.files.get('file')
+    if not file_upload:
+        # This might happen when trying to upload file names
+        # with non-ascii characters on Safari.
+        return _("No file content. Delete non ASCII characters from the file name and try again.")
+
     try:
-        overwrite = int(overwrite)
+        overwrite = int(form.get('overwrite', '0'))
     except:
         overwrite = 0
 
@@ -581,94 +569,86 @@
     if overwrite and not request.user.may.delete(pagename):
         return _('You are not allowed to overwrite a file attachment of this page.')
 
-    filename = form.get('file__filename__', u'')
-    rename = form.get('rename', [u''])[0].strip()
-    if rename:
-        target = rename
-    else:
-        target = filename
+    target = form.get('target', u'').strip()
+    if not target:
+        target = file_upload.filename or u''
 
-    target = preprocess_filename(target)
     target = wikiutil.clean_input(target)
 
     if not target:
         return _("Filename of attachment not specified!")
 
-    # get file content
-    filecontent = request.form.get('file', [None])[0]
-    if filecontent is None:
-        # This might happen when trying to upload file names
-        # with non-ascii characters on Safari.
-        return _("No file content. Delete non ASCII characters from the file name and try again.")
-
     # add the attachment
     try:
-        target, bytes = add_attachment(request, pagename, target, filecontent, overwrite=overwrite)
+        target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
         msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
                 " with %(bytes)d bytes saved.") % {
-                'target': target, 'filename': filename, 'bytes': bytes}
+                'target': target, 'filename': file_upload.filename, 'bytes': bytes}
     except AttachmentAlreadyExists:
         msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
-            'target': target, 'filename': filename}
+            'target': target, 'filename': file_upload.filename}
 
     # return attachment list
     upload_form(pagename, request, msg)
 
 
-def _do_savedrawing(pagename, request):
-    _ = request.getText
-
-    if not wikiutil.checkTicket(request, request.form.get('ticket', [''])[0]):
-        return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.savedrawing' }
-
-    if not request.user.may.write(pagename):
-        return _('You are not allowed to save a drawing on this page.')
-
-    filename = request.form['filename'][0]
-    filecontent = request.form['filepath'][0]
-
-    basepath, basename = os.path.split(filename)
-    basename, ext = os.path.splitext(basename)
-
-    # get directory, and possibly create it
-    attach_dir = getAttachDir(request, pagename, create=1)
-    savepath = os.path.join(attach_dir, basename + ext)
+class ContainerItem:
+    """ A storage container (multiple objects in 1 tarfile) """
 
-    if ext == '.draw':
-        _addLogEntry(request, 'ATTDRW', pagename, basename + ext)
-        filecontent = filecontent.read() # read file completely into memory
-        filecontent = filecontent.replace("\r", "")
-    elif ext == '.map':
-        filecontent = filecontent.read() # read file completely into memory
-        filecontent = filecontent.strip()
+    def __init__(self, request, pagename, containername):
+        self.request = request
+        self.pagename = pagename
+        self.containername = containername
+        self.container_filename = getFilename(request, pagename, containername)
 
-    if filecontent:
-        # filecontent is either a file or a non-empty string
-        stream = open(savepath, 'wb')
-        try:
-            _write_stream(filecontent, stream)
-        finally:
-            stream.close()
-    else:
-        # filecontent is empty string (e.g. empty map file), delete the target file
-        try:
-            os.unlink(savepath)
-        except OSError, err:
-            if err.errno != errno.ENOENT: # no such file
-                raise
+    def member_url(self, member):
+        """ return URL for accessing container member
+            (we use same URL for get (GET) and put (POST))
+        """
+        url = Page(self.request, self.pagename).url(self.request, {
+            'action': 'AttachFile',
+            'do': 'box',  # shorter to type than 'container'
+            'target': self.containername,
+            #'member': member,
+        })
+        return url + '&member=%s' % member
+        # member needs to be last in qs because twikidraw looks for "file extension" at the end
 
-    # touch attachment directory to invalidate cache if new map is saved
-    if ext == '.map':
-        os.utime(attach_dir, None)
+    def get(self, member):
+        """ return a file-like object with the member file data
+        """
+        tf = tarfile.TarFile(self.container_filename)
+        return tf.extractfile(member)
 
-    request.emit_http_headers()
-    request.write("OK")
+    def put(self, member, content, content_length=None):
+        """ save data into a container's member """
+        tf = tarfile.TarFile(self.container_filename, mode='a')
+        if isinstance(member, unicode):
+            member = member.encode('utf-8')
+        ti = tarfile.TarInfo(member)
+        if isinstance(content, str):
+            if content_length is None:
+                content_length = len(content)
+            content = StringIO(content) # we need a file obj
+        elif not hasattr(content, 'read'):
+            logging.error("unsupported content object: %r" % content)
+            raise
+        assert content_length >= 0  # we don't want -1 interpreted as 4G-1
+        ti.size = content_length
+        tf.addfile(ti, content)
+        tf.close()
 
+    def truncate(self):
+        f = open(self.container_filename, 'w')
+        f.close()
+
+    def exists(self):
+        return os.path.exists(self.container_filename)
 
 def _do_del(pagename, request):
     _ = request.getText
 
-    if not wikiutil.checkTicket(request, request.form.get('ticket', [''])[0]):
+    if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.del' }
 
     pagename, filename, fpath = _access_file(pagename, request)
@@ -677,15 +657,7 @@
     if not filename:
         return # error msg already sent in _access_file
 
-    # delete file
-    os.remove(fpath)
-    _addLogEntry(request, 'ATTDEL', pagename, filename)
-
-    if request.cfg.xapian_search:
-        from MoinMoin.search.Xapian import Index
-        index = Index(request)
-        if index.exists:
-            index.remove_item(pagename, filename)
+    remove_attachment(request, pagename, filename)
 
     upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
 
@@ -708,10 +680,14 @@
             return
 
         if new_attachment_path != attachment_path:
-            # move file
+            filesize = os.path.getsize(attachment_path)
             filesys.rename(attachment_path, new_attachment_path)
             _addLogEntry(request, 'ATTDEL', pagename, attachment)
+            event = FileRemovedEvent(request, pagename, attachment, filesize)
+            send_event(event)
             _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
+            event = FileAttachedEvent(request, new_pagename, new_attachment, filesize)
+            send_event(event)
             upload_form(pagename, request,
                         msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
                             'pagename': pagename,
@@ -730,17 +706,17 @@
 
     if 'cancel' in request.form:
         return _('Move aborted!')
-    if not wikiutil.checkTicket(request, request.form.get('ticket', [''])[0]):
+    if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.move' }
     if not request.user.may.delete(pagename):
         return _('You are not allowed to move attachments from this page.')
 
     if 'newpagename' in request.form:
-        new_pagename = request.form.get('newpagename')[0]
+        new_pagename = request.form.get('newpagename')
     else:
         upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
     if 'newattachmentname' in request.form:
-        new_attachment = request.form.get('newattachmentname')[0]
+        new_attachment = request.form.get('newattachmentname')
         if new_attachment != wikiutil.taintfilename(new_attachment):
             upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
                                   'filename': new_attachment})
@@ -748,7 +724,7 @@
     else:
         upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
 
-    attachment = request.form.get('oldattachmentname')[0]
+    attachment = request.form.get('oldattachmentname')
     move_file(request, pagename, new_pagename, attachment, new_attachment)
 
 
@@ -763,11 +739,10 @@
 
     # move file
     d = {'action': action_name,
-         'baseurl': request.getScriptname(),
+         'url': request.href(pagename),
          'do': 'attachment_move',
          'ticket': wikiutil.createTicket(request),
          'pagename': wikiutil.escape(pagename, 1),
-         'pagename_quoted': wikiutil.quoteWikinameURL(pagename),
          'attachment_name': wikiutil.escape(filename, 1),
          'move': _('Move'),
          'cancel': _('Cancel'),
@@ -775,7 +750,7 @@
          'attachment_label': _("New attachment name"),
         }
     formhtml = '''
-<form action="%(baseurl)s/%(pagename_quoted)s" method="POST">
+<form action="%(url)s" method="POST">
 <input type="hidden" name="action" value="%(action)s">
 <input type="hidden" name="do" value="%(do)s">
 <input type="hidden" name="ticket" value="%(ticket)s">
@@ -807,6 +782,49 @@
     return thispage.send_page()
 
 
+def _do_box(pagename, request):
+    _ = request.getText
+
+    pagename, filename, fpath = _access_file(pagename, request)
+    if not request.user.may.read(pagename):
+        return _('You are not allowed to get attachments from this page.')
+    if not filename:
+        return # error msg already sent in _access_file
+
+    timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
+    if_modified = request.if_modified_since
+    if if_modified and if_modified >= timestamp:
+        request.status_code = 304
+    else:
+        ci = ContainerItem(request, pagename, filename)
+        filename = wikiutil.taintfilename(request.values['member'])
+        mt = wikiutil.MimeType(filename=filename)
+        content_type = mt.content_type()
+        mime_type = mt.mime_type()
+
+        # 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
+        filename_enc = filename.encode(config.charset)
+
+        # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
+        # we just let the user store them to disk ('attachment').
+        # For safe files, we directly show them inline (this also works better for IE).
+        dangerous = mime_type in request.cfg.mimetypes_xss_protect
+        content_dispo = dangerous and 'attachment' or 'inline'
+
+        now = time.time()
+        request.headers.add('Date', http_date(now))
+        request.headers.add('Content-Type', content_type)
+        request.headers.add('Last-Modified', http_date(timestamp))
+        request.headers.add('Expires', http_date(now - 365 * 24 * 3600))
+        #request.headers.add('Content-Length', os.path.getsize(fpath))
+        content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
+        request.headers.add('Content-Disposition', content_dispo_string)
+
+        # send data
+        request.send_file(ci.get(filename))
+
+
 def _do_get(pagename, request):
     _ = request.getText
 
@@ -816,9 +834,10 @@
     if not filename:
         return # error msg already sent in _access_file
 
-    timestamp = timefuncs.formathttpdate(int(os.path.getmtime(fpath)))
-    if request.if_modified_since == timestamp:
-        request.emit_http_headers(["Status: 304 Not modified"])
+    timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
+    if_modified = request.if_modified_since
+    if if_modified and if_modified >= timestamp:
+        request.status_code = 304
     else:
         mt = wikiutil.MimeType(filename=filename)
         content_type = mt.content_type()
@@ -834,12 +853,14 @@
         dangerous = mime_type in request.cfg.mimetypes_xss_protect
         content_dispo = dangerous and 'attachment' or 'inline'
 
-        request.emit_http_headers([
-            'Content-Type: %s' % content_type,
-            'Last-Modified: %s' % timestamp,
-            'Content-Length: %d' % os.path.getsize(fpath),
-            'Content-Disposition: %s; filename="%s"' % (content_dispo, filename_enc),
-        ])
+        now = time.time()
+        request.headers.add('Date', http_date(now))
+        request.headers.add('Content-Type', content_type)
+        request.headers.add('Last-Modified', http_date(timestamp))
+        request.headers.add('Expires', http_date(now - 365 * 24 * 3600))
+        request.headers.add('Content-Length', os.path.getsize(fpath))
+        content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
+        request.headers.add('Content-Disposition', content_dispo_string)
 
         # send data
         request.send_file(open(fpath, 'rb'))
@@ -848,7 +869,7 @@
 def _do_install(pagename, request):
     _ = request.getText
 
-    if not wikiutil.checkTicket(request, request.form.get('ticket', [''])[0]):
+    if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.install' }
 
     pagename, target, targetpath = _access_file(pagename, request)
@@ -875,7 +896,7 @@
 def _do_unzip(pagename, request, overwrite=False):
     _ = request.getText
 
-    if not wikiutil.checkTicket(request, request.form.get('ticket', [''])[0]):
+    if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
         return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.unzip' }
 
     pagename, filename, fpath = _access_file(pagename, request)
@@ -993,12 +1014,17 @@
             fmt.url(0))
     request.write('%s<br><br>' % link)
 
+    if filename.endswith('.tdraw') or filename.endswith('.adraw'):
+        request.write(fmt.attachment_drawing(filename, ''))
+        return
+
     mt = wikiutil.MimeType(filename=filename)
 
     # destinguishs if browser need a plugin in place
     if mt.major == 'image' and mt.minor in config.browser_supported_images:
+        url = getAttachUrl(pagename, filename, request)
         request.write('<img src="%s" alt="%s">' % (
-            getAttachUrl(pagename, filename, request, escaped=1),
+            wikiutil.escape(url, 1),
             wikiutil.escape(filename, 1)))
         return
     elif mt.major == 'text':
@@ -1078,8 +1104,9 @@
     if not filename:
         return
 
+    request.formatter.page = Page(request, pagename)
+
     # send header & title
-    request.emit_http_headers()
     # Use user interface language for this generated page
     request.setContentLanguage(request.lang)
     title = _('attachment:%(filename)s of %(pagename)s') % {
--- a/MoinMoin/action/CopyPage.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/CopyPage.py	Mon Feb 08 21:49:19 2010 +0100
@@ -52,20 +52,19 @@
             return status, _('TextCha: Wrong answer! Go back and try again...')
 
         form = self.form
-        newpagename = form.get('newpagename', [u''])[0]
-        newpagename = self.request.normalizePagename(newpagename)
-        comment = form.get('comment', [u''])[0]
+        newpagename = form.get('newpagename', u'')
+        newpagename = wikiutil.normalize_pagename(newpagename, self.cfg)
+        comment = form.get('comment', u'')
         comment = wikiutil.clean_input(comment)
 
         self.page = PageEditor(self.request, self.pagename)
         success, msgs = self.page.copyPage(newpagename, comment)
 
         copy_subpages = 0
-        if form.has_key('copy_subpages'):
-            try:
-                copy_subpages = int(form['copy_subpages'][0])
-            except:
-                pass
+        try:
+            copy_subpages = int(form['copy_subpages'])
+        except:
+            pass
 
         if copy_subpages and self.subpages or (not self.users_subpages and self.subpages):
             for name in self.subpages:
@@ -92,7 +91,7 @@
             d = {
                 'textcha': TextCha(self.request).render(),
                 'subpage': subpages,
-                'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', ['0'])[0] == '1'],
+                'subpages_checked': ('', 'checked')[self.request.args.get('subpages_checked', '0') == '1'],
                 'subpage_label': _('Copy all /subpages too?'),
                 'pagename': wikiutil.escape(self.pagename, True),
                 'newname_label': _("New name"),
--- a/MoinMoin/action/DeletePage.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/DeletePage.py	Mon Feb 08 21:49:19 2010 +0100
@@ -44,7 +44,7 @@
     def do_action(self):
         """ Delete pagename """
         form = self.form
-        comment = form.get('comment', [u''])[0]
+        comment = form.get('comment', u'')
         comment = wikiutil.clean_input(comment)
 
         # Create a page editor that does not do editor backups, because
@@ -53,11 +53,10 @@
         success, msgs = self.page.deletePage(comment)
 
         delete_subpages = 0
-        if 'delete_subpages' in form:
-            try:
-                delete_subpages = int(form['delete_subpages'][0])
-            except:
-                pass
+        try:
+            delete_subpages = int(form['delete_subpages'])
+        except:
+            pass
 
         if delete_subpages and self.subpages:
             for name in self.subpages:
@@ -75,7 +74,7 @@
 
             d = {
                 'subpage': subpages,
-                'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', ['0'])[0] == '1'],
+                'subpages_checked': ('', 'checked')[self.request.args.get('subpages_checked', '0') == '1'],
                 'subpage_label': _('Delete all /subpages too?'),
                 'comment_label': _("Optional reason for the deletion"),
                 'buttons_html': buttons_html,
--- a/MoinMoin/action/Despam.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/Despam.py	Mon Feb 08 21:49:19 2010 +0100
@@ -12,6 +12,9 @@
 
 import time
 
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
 from MoinMoin.logfile import editlog
 from MoinMoin.util.dataset import TupleDataset, Column
 from MoinMoin.widget.browser import DataBrowserWidget
@@ -66,7 +69,7 @@
             pg.link_to(request, text=_("Select Author"),
                 querystr={
                     'action': 'Despam',
-                    'editor': editor, # was: url_quote_plus()
+                    'editor': repr(editor),
                 })))
 
     table = DataBrowserWidget(request)
@@ -112,7 +115,7 @@
 </form>
 </p>
 ''' % dict(
-        url="%s/%s" % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename)),
+        url=request.href(pagename),
         ticket=wikiutil.createTicket(request),
         editor=wikiutil.url_quote(editor),
         label=_("Revert all!"),
@@ -156,7 +159,7 @@
 def revert_pages(request, editor, timestamp):
     _ = request.getText
 
-    editor = wikiutil.url_unquote(editor, want_unicode=False)
+    editor = wikiutil.url_unquote(editor)
     timestamp = int(timestamp * 1000000)
     log = editlog.EditLog(request)
     pages = {}
@@ -189,17 +192,17 @@
         request.theme.add_msg(_('You are not allowed to use this action.'), "error")
         return Page.Page(request, pagename).send_page()
 
-    editor = request.form.get('editor', [None])[0]
+    editor = request.values.get('editor')
     timestamp = time.time() - DAYS * 24 * 3600
-    ok = request.form.get('ok', [0])[0]
+    ok = request.form.get('ok', 0)
+    logging.debug("editor: %r ok: %r" % (editor, ok))
 
-    request.emit_http_headers()
     request.theme.send_title("Despam", pagename=pagename)
     # Start content (important for RTL support)
     request.write(request.formatter.startContent("content"))
 
-    if (request.request_method == 'POST' and ok and
-        wikiutil.checkTicket(request, request.form.get('ticket', [''])[0])):
+    if (request.method == 'POST' and ok and
+        wikiutil.checkTicket(request, request.form.get('ticket', ''))):
         revert_pages(request, editor, timestamp)
     elif editor:
         show_pages(request, pagename, editor, timestamp)
--- a/MoinMoin/action/LikePages.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/LikePages.py	Mon Feb 08 21:49:19 2010 +0100
@@ -41,8 +41,6 @@
         return
 
     # more than one match, list 'em
-    request.emit_http_headers()
-
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
 
--- a/MoinMoin/action/Load.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/Load.py	Mon Feb 08 21:49:19 2010 +0100
@@ -41,23 +41,26 @@
         if not TextCha(request).check_answer_from_form():
             return status, _('TextCha: Wrong answer! Go back and try again...')
 
-        comment = form.get('comment', [u''])[0]
+        comment = form.get('comment', u'')
         comment = wikiutil.clean_input(comment)
 
-        filename = form.get('file__filename__')
-        rename = form.get('rename', [''])[0].strip()
+        file_upload = request.files.get('file')
+        if not file_upload:
+            # This might happen when trying to upload file names
+            # with non-ascii characters on Safari.
+            return False, _("No file content. Delete non ASCII characters from the file name and try again.")
+
+        filename = file_upload.filename
+        rename = form.get('rename', '').strip()
         if rename:
             target = rename
         else:
             target = filename
 
-        target = AttachFile.preprocess_filename(target)
         target = wikiutil.clean_input(target)
 
         if target:
-            filecontent = form['file'][0]
-            if hasattr(filecontent, 'read'): # a file-like object
-                filecontent = filecontent.read() # XXX reads complete file into memory!
+            filecontent = file_upload.stream.read() # XXX reads complete file into memory!
             filecontent = wikiutil.decodeUnknownInput(filecontent)
 
             self.pagename = target
--- a/MoinMoin/action/LocalSiteMap.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/LocalSiteMap.py	Mon Feb 08 21:49:19 2010 +0100
@@ -31,7 +31,6 @@
 
 def execute(pagename, request):
     _ = request.getText
-    request.emit_http_headers()
 
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
@@ -69,12 +68,13 @@
         """
         if not name:
             return
+        _ = request.getText
         pg = Page(request, name)
         action = __name__.split('.')[-1]
         self.append('&nbsp;' * (5*depth+1))
         self.append(pg.link_to(request, querystr={'action': action}))
         self.append("&nbsp;<small>[")
-        self.append(pg.link_to(request, 'view'))
+        self.append(pg.link_to(request, _('view')))
         self.append("</small>]<br>")
 
     def append(self, text):
--- a/MoinMoin/action/MyPages.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/MyPages.py	Mon Feb 08 21:49:19 2010 +0100
@@ -58,7 +58,6 @@
     pagecontent = pagecontent.replace('\n', '\r\n')
 
     from MoinMoin.parser.text_moin_wiki import Parser as WikiParser
-    request.emit_http_headers()
 
     # This action generate data using the user language
     request.setContentLanguage(request.lang)
--- a/MoinMoin/action/PackagePages.py	Mon Feb 08 19:01:03 2010 +0100
+++ b/MoinMoin/action/PackagePages.py	Mon Feb 08 21:49:19 2010 +0100
@@ -7,9 +7,10 @@
     TODO: use ActionBase class
 
     @copyright: 2005 MoinMoin:AlexanderSchremmer
+                2007-2009 MoinMoin:ReimarBauer
     @license: GNU GPL, see COPYING for details.
 """
-
+import cStringIO
 import os
 import zipfile
 from datetime import datetime