changeset 0:5568cf133caf moin20-repo-reboot

create moin/2.0 repo, drop all history (see notes below) Up to now, we used the moin/2.0-dev repository (which was cloned from another, older moin repo quite some time ago). Over the years, these repositories got rather fat (>200MB) and were a pain to clone over slow, high-latency or unreliable connections. After having finished most of the dirty work in moin2, having killed all the 3rd party code we had bundled with (is now installed by quickinstall / pip / setuptools), it is now a good time to get rid of the history (the history made up most of the repository's size). If you need to look at the history, look there: http://hg.moinmo.in/moin/2.0-dev The new moin/2.0 repository has the files as of this changesets: http://hg.moinmo.in/moin/2.0-dev/rev/075132a755dc The changeset hashes that link the repositories will be tagged (in both repositories) as "moin20-repo-reboot".
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sun, 20 Feb 2011 20:53:45 +0100
parents
children b55d0798d966
files .hgignore Makefile MoinMoin/__init__.py MoinMoin/_template.py MoinMoin/_tests/__init__.py MoinMoin/_tests/_test_template.py MoinMoin/_tests/ldap_testbase.py MoinMoin/_tests/ldap_testdata.py MoinMoin/_tests/maketestwiki.py MoinMoin/_tests/pep8.py MoinMoin/_tests/test_error.py MoinMoin/_tests/test_sourcecode.py MoinMoin/_tests/test_test_environ.py MoinMoin/_tests/test_user.py MoinMoin/_tests/test_wikiutil.py MoinMoin/_tests/testitems.xml MoinMoin/_tests/wiki/data/plugin/__init__.py MoinMoin/_tests/wikiconfig.py MoinMoin/app.py MoinMoin/apps/__init__.py MoinMoin/apps/admin/__init__.py MoinMoin/apps/admin/templates/index.html MoinMoin/apps/admin/templates/sysitems_upgrade.html MoinMoin/apps/admin/templates/userbrowser.html MoinMoin/apps/admin/views.py MoinMoin/apps/feed/__init__.py MoinMoin/apps/feed/_tests/test_feed.py MoinMoin/apps/feed/views.py MoinMoin/apps/frontend/__init__.py MoinMoin/apps/frontend/_tests/test_frontend.py MoinMoin/apps/frontend/views.py MoinMoin/apps/misc/__init__.py MoinMoin/apps/misc/_tests/test_misc.py MoinMoin/apps/misc/templates/sitemap.xml MoinMoin/apps/misc/templates/urls_names.txt MoinMoin/apps/misc/views.py MoinMoin/apps/serve/__init__.py MoinMoin/apps/serve/views.py MoinMoin/auth/__init__.py MoinMoin/auth/_tests/test_ldap_login.py MoinMoin/auth/http.py MoinMoin/auth/ldap_login.py MoinMoin/auth/log.py MoinMoin/auth/openidrp.py MoinMoin/auth/smb_mount.py MoinMoin/config/__init__.py MoinMoin/config/_tests/test_defaultconfig.py MoinMoin/config/default.py MoinMoin/conftest.py MoinMoin/converter/__init__.py MoinMoin/converter/_args.py MoinMoin/converter/_args_wiki.py MoinMoin/converter/_table.py MoinMoin/converter/_tests/__init__.py MoinMoin/converter/_tests/test__args.py MoinMoin/converter/_tests/test__args_wiki.py MoinMoin/converter/_tests/test__wiki_macro.py MoinMoin/converter/_tests/test_creole_in.py MoinMoin/converter/_tests/test_docbook_in.py MoinMoin/converter/_tests/test_docbook_out.py MoinMoin/converter/_tests/test_html_in.py MoinMoin/converter/_tests/test_html_in_out.py MoinMoin/converter/_tests/test_html_out.py MoinMoin/converter/_tests/test_include.py MoinMoin/converter/_tests/test_link.py MoinMoin/converter/_tests/test_mediawiki_in.py MoinMoin/converter/_tests/test_moinwiki19_in.py MoinMoin/converter/_tests/test_moinwiki_in.py MoinMoin/converter/_tests/test_moinwiki_in_out.py MoinMoin/converter/_tests/test_moinwiki_out.py MoinMoin/converter/_tests/test_rst_in.py MoinMoin/converter/_tests/test_rst_out.py MoinMoin/converter/_tests/test_smiley.py MoinMoin/converter/_wiki_macro.py MoinMoin/converter/archive_in.py MoinMoin/converter/audio_video_in.py MoinMoin/converter/creole_in.py MoinMoin/converter/docbook_in.py MoinMoin/converter/docbook_out.py MoinMoin/converter/everything.py MoinMoin/converter/highlight.py MoinMoin/converter/html_in.py MoinMoin/converter/html_out.py MoinMoin/converter/image_in.py MoinMoin/converter/include.py MoinMoin/converter/link.py MoinMoin/converter/macro.py MoinMoin/converter/mediawiki_in.py MoinMoin/converter/moinwiki19_in.py MoinMoin/converter/moinwiki_in.py MoinMoin/converter/moinwiki_out.py MoinMoin/converter/nonexistent_in.py MoinMoin/converter/pygments_in.py MoinMoin/converter/rst_in.py MoinMoin/converter/rst_out.py MoinMoin/converter/smiley.py MoinMoin/converter/text_csv_in.py MoinMoin/converter/text_in.py MoinMoin/datastruct/__init__.py MoinMoin/datastruct/backends/__init__.py MoinMoin/datastruct/backends/_tests/__init__.py MoinMoin/datastruct/backends/_tests/test_composite_dicts.py MoinMoin/datastruct/backends/_tests/test_composite_groups.py MoinMoin/datastruct/backends/_tests/test_config_dicts.py MoinMoin/datastruct/backends/_tests/test_config_groups.py MoinMoin/datastruct/backends/_tests/test_lazy_config_groups.py MoinMoin/datastruct/backends/_tests/test_wiki_dicts.py MoinMoin/datastruct/backends/_tests/test_wiki_groups.py MoinMoin/datastruct/backends/composite_dicts.py MoinMoin/datastruct/backends/composite_groups.py MoinMoin/datastruct/backends/config_dicts.py MoinMoin/datastruct/backends/config_groups.py MoinMoin/datastruct/backends/config_lazy_groups.py MoinMoin/datastruct/backends/wiki_dicts.py MoinMoin/datastruct/backends/wiki_groups.py MoinMoin/error.py MoinMoin/i18n/__init__.py MoinMoin/items/__init__.py MoinMoin/items/_tests/test_Item.py MoinMoin/log.py MoinMoin/macro/Anchor.py MoinMoin/macro/Date.py MoinMoin/macro/DateTime.py MoinMoin/macro/EditedSystemPages.py MoinMoin/macro/GetText.py MoinMoin/macro/GetVal.py MoinMoin/macro/GoTo.py MoinMoin/macro/HighlighterList.py MoinMoin/macro/InterWiki.py MoinMoin/macro/MailTo.py MoinMoin/macro/PageCount.py MoinMoin/macro/PageSize.py MoinMoin/macro/PagenameList.py MoinMoin/macro/RandomItem.py MoinMoin/macro/TemplateList.py MoinMoin/macro/Verbatim.py MoinMoin/macro/WikiConfig.py MoinMoin/macro/WikiConfigHelp.py MoinMoin/macro/__init__.py MoinMoin/macro/_base.py MoinMoin/macro/_tests/test__base.py MoinMoin/mail/__init__.py MoinMoin/mail/_tests/test_sendmail.py MoinMoin/mail/sendmail.py MoinMoin/script/__init__.py MoinMoin/script/account/__init__.py MoinMoin/script/account/create.py MoinMoin/script/account/disable.py MoinMoin/script/account/resetpw.py MoinMoin/script/index/__init__.py MoinMoin/script/index/build.py MoinMoin/script/maint/__init__.py MoinMoin/script/maint/meta.py MoinMoin/script/maint/reducewiki.py MoinMoin/script/maint/xml.py MoinMoin/script/migration/__init__.py MoinMoin/script/migration/backend.py MoinMoin/script/moin.py MoinMoin/script/old/__init__.py MoinMoin/script/old/print_stats.py MoinMoin/search/Xapian/__init__.py MoinMoin/search/Xapian/_tests/__init__.py MoinMoin/search/Xapian/indexing.py MoinMoin/search/Xapian/search.py MoinMoin/search/Xapian/tokenizer.py MoinMoin/search/__init__.py MoinMoin/search/_tests/test_search.py MoinMoin/search/_tests/test_terms.py MoinMoin/search/_tests/test_wiki_analyzer.py MoinMoin/search/builtin.py MoinMoin/search/queryparser/__init__.py MoinMoin/search/queryparser/expressions.py MoinMoin/search/results.py MoinMoin/search/term.py MoinMoin/security/__init__.py MoinMoin/security/_tests/test_security.py MoinMoin/security/_tests/test_ticket.py MoinMoin/security/textcha.py MoinMoin/security/ticket.py MoinMoin/signalling/__init__.py MoinMoin/signalling/log.py MoinMoin/signalling/signals.py MoinMoin/static/js/common.js MoinMoin/static/js/countdown.js MoinMoin/static/logos/favicon.ico MoinMoin/static/logos/moindude.png MoinMoin/static/logos/moinmoin.png MoinMoin/static/logos/moinmoin.svg MoinMoin/static/logos/moinmoin_alpha.png MoinMoin/static/logos/moinmoin_powered.png MoinMoin/static/logos/python_powered.png MoinMoin/storage/__init__.py MoinMoin/storage/_tests/__init__.py MoinMoin/storage/_tests/test_backends.py MoinMoin/storage/_tests/test_backends_flatfile.py MoinMoin/storage/_tests/test_backends_fs.py MoinMoin/storage/_tests/test_backends_fs19.py MoinMoin/storage/_tests/test_backends_fs2.py MoinMoin/storage/_tests/test_backends_hg.py MoinMoin/storage/_tests/test_backends_memory.py MoinMoin/storage/_tests/test_backends_router.py MoinMoin/storage/_tests/test_backends_sqla.py MoinMoin/storage/_tests/test_middleware_acl.py MoinMoin/storage/_tests/test_serialization.py MoinMoin/storage/_tests/tests_backend_api.py MoinMoin/storage/backends/__init__.py MoinMoin/storage/backends/_flatutils.py MoinMoin/storage/backends/_fsutils.py MoinMoin/storage/backends/acl.py MoinMoin/storage/backends/fileserver.py MoinMoin/storage/backends/flatfile.py MoinMoin/storage/backends/fs.py MoinMoin/storage/backends/fs19.py MoinMoin/storage/backends/fs19_logfile.py MoinMoin/storage/backends/fs2.py MoinMoin/storage/backends/hg.py MoinMoin/storage/backends/indexing.py MoinMoin/storage/backends/memory.py MoinMoin/storage/backends/router.py MoinMoin/storage/backends/sqla.py MoinMoin/storage/error.py MoinMoin/storage/serialization.py MoinMoin/templates/base.html MoinMoin/templates/copy.html MoinMoin/templates/delete.html MoinMoin/templates/destroy.html MoinMoin/templates/diff.html MoinMoin/templates/diff_text.html MoinMoin/templates/dom.xml MoinMoin/templates/editbar.html MoinMoin/templates/error.html MoinMoin/templates/forms.html MoinMoin/templates/global_history.html MoinMoin/templates/global_index.html MoinMoin/templates/global_tags.html MoinMoin/templates/highlight.html MoinMoin/templates/history.html MoinMoin/templates/index.html MoinMoin/templates/item_link_list.html MoinMoin/templates/layout.html MoinMoin/templates/login.html MoinMoin/templates/lostpass.html MoinMoin/templates/meta.html MoinMoin/templates/modify_anywikidraw.html MoinMoin/templates/modify_applet.html MoinMoin/templates/modify_binary.html MoinMoin/templates/modify_show_template_selection.html MoinMoin/templates/modify_show_type_selection.html MoinMoin/templates/modify_svg-edit.html MoinMoin/templates/modify_text.html MoinMoin/templates/modify_text_html.html MoinMoin/templates/modify_twikidraw.html MoinMoin/templates/openid_register.html MoinMoin/templates/recoverpass.html MoinMoin/templates/register.html MoinMoin/templates/rename.html MoinMoin/templates/revert.html MoinMoin/templates/show.html MoinMoin/templates/sitemap.html MoinMoin/templates/snippets.html MoinMoin/templates/usersettings.html MoinMoin/templates/utils.html MoinMoin/templates/wanteds.html MoinMoin/themes/__init__.py MoinMoin/themes/_tests/test_navi_bar.py MoinMoin/themes/modernized/info.json MoinMoin/themes/modernized/static/css/common.css MoinMoin/themes/modernized/static/css/msie.css MoinMoin/themes/modernized/static/css/print.css MoinMoin/themes/modernized/static/css/projection.css MoinMoin/themes/modernized/static/css/screen.css MoinMoin/themes/modernized/static/img/PythonPowered.png MoinMoin/themes/modernized/static/img/admonitions/caution.png MoinMoin/themes/modernized/static/img/admonitions/important.png MoinMoin/themes/modernized/static/img/admonitions/note.png MoinMoin/themes/modernized/static/img/admonitions/tip.png MoinMoin/themes/modernized/static/img/admonitions/warning.png MoinMoin/themes/modernized/static/img/attach.png MoinMoin/themes/modernized/static/img/draft.png MoinMoin/themes/modernized/static/img/moin-action.png MoinMoin/themes/modernized/static/img/moin-attach.png MoinMoin/themes/modernized/static/img/moin-bottom.png MoinMoin/themes/modernized/static/img/moin-conflict.png MoinMoin/themes/modernized/static/img/moin-deleted.png MoinMoin/themes/modernized/static/img/moin-diff.png MoinMoin/themes/modernized/static/img/moin-download.png MoinMoin/themes/modernized/static/img/moin-edit.png MoinMoin/themes/modernized/static/img/moin-email.png MoinMoin/themes/modernized/static/img/moin-ftp.png MoinMoin/themes/modernized/static/img/moin-help.png MoinMoin/themes/modernized/static/img/moin-home.png MoinMoin/themes/modernized/static/img/moin-icon.png MoinMoin/themes/modernized/static/img/moin-info.png MoinMoin/themes/modernized/static/img/moin-inter.png MoinMoin/themes/modernized/static/img/moin-jabber.png MoinMoin/themes/modernized/static/img/moin-new.png MoinMoin/themes/modernized/static/img/moin-news.png MoinMoin/themes/modernized/static/img/moin-parent.png MoinMoin/themes/modernized/static/img/moin-print.png MoinMoin/themes/modernized/static/img/moin-raw.png MoinMoin/themes/modernized/static/img/moin-readonly.png MoinMoin/themes/modernized/static/img/moin-renamed.png MoinMoin/themes/modernized/static/img/moin-rss.png MoinMoin/themes/modernized/static/img/moin-search.png MoinMoin/themes/modernized/static/img/moin-show.png MoinMoin/themes/modernized/static/img/moin-subscribe.png MoinMoin/themes/modernized/static/img/moin-telnet.png MoinMoin/themes/modernized/static/img/moin-top.png MoinMoin/themes/modernized/static/img/moin-unsubscribe.png MoinMoin/themes/modernized/static/img/moin-up.png MoinMoin/themes/modernized/static/img/moin-updated.png MoinMoin/themes/modernized/static/img/moin-www.png MoinMoin/themes/modernized/static/img/smileys/alert.png MoinMoin/themes/modernized/static/img/smileys/angry.png MoinMoin/themes/modernized/static/img/smileys/attention.png MoinMoin/themes/modernized/static/img/smileys/biggrin.png MoinMoin/themes/modernized/static/img/smileys/checkmark.png MoinMoin/themes/modernized/static/img/smileys/devil.png MoinMoin/themes/modernized/static/img/smileys/frown.png MoinMoin/themes/modernized/static/img/smileys/icon-error.png MoinMoin/themes/modernized/static/img/smileys/icon-info.png MoinMoin/themes/modernized/static/img/smileys/idea.png MoinMoin/themes/modernized/static/img/smileys/ohwell.png MoinMoin/themes/modernized/static/img/smileys/prio1.png MoinMoin/themes/modernized/static/img/smileys/prio2.png MoinMoin/themes/modernized/static/img/smileys/prio3.png MoinMoin/themes/modernized/static/img/smileys/redface.png MoinMoin/themes/modernized/static/img/smileys/sad.png MoinMoin/themes/modernized/static/img/smileys/smile.png MoinMoin/themes/modernized/static/img/smileys/smile2.png MoinMoin/themes/modernized/static/img/smileys/smile3.png MoinMoin/themes/modernized/static/img/smileys/smile4.png MoinMoin/themes/modernized/static/img/smileys/star_off.png MoinMoin/themes/modernized/static/img/smileys/star_on.png MoinMoin/themes/modernized/static/img/smileys/thumbs-up.png MoinMoin/themes/modernized/static/img/smileys/tired.png MoinMoin/themes/modernized/static/img/smileys/tongue.png MoinMoin/themes/modernized/static/img/white_clouds.png MoinMoin/translations/MoinMoin.pot MoinMoin/translations/de/LC_MESSAGES/messages.po MoinMoin/user.py MoinMoin/util/SubProcess.py MoinMoin/util/__init__.py MoinMoin/util/_tests/test_diff3.py MoinMoin/util/_tests/test_diff_text.py MoinMoin/util/_tests/test_filesys.py MoinMoin/util/_tests/test_interwiki.py MoinMoin/util/_tests/test_iri.py MoinMoin/util/_tests/test_lock.py MoinMoin/util/_tests/test_mime.py MoinMoin/util/_tests/test_paramparser.py MoinMoin/util/_tests/test_pysupport.py MoinMoin/util/_tests/test_registry.py MoinMoin/util/_tests/test_tree.py MoinMoin/util/_tests/test_util.py MoinMoin/util/_tests/test_version.py MoinMoin/util/chartypes.py MoinMoin/util/chartypes_create.py MoinMoin/util/clock.py MoinMoin/util/diff3.py MoinMoin/util/diff_html.py MoinMoin/util/diff_text.py MoinMoin/util/edit_lock.py MoinMoin/util/filesys.py MoinMoin/util/forms.py MoinMoin/util/interwiki.py MoinMoin/util/iri.py MoinMoin/util/kvstore.py MoinMoin/util/lock.py MoinMoin/util/md5crypt.py MoinMoin/util/mime.py MoinMoin/util/monkeypatch.py MoinMoin/util/paramparser.py MoinMoin/util/plugins.py MoinMoin/util/profile.py MoinMoin/util/pycdb.py MoinMoin/util/pysupport.py MoinMoin/util/python_compatibility.py MoinMoin/util/registry.py MoinMoin/util/send_file.py MoinMoin/util/thread_monitor.py MoinMoin/util/tree.py MoinMoin/util/version.py MoinMoin/wikiutil.py README.txt contrib/images/logos/README contrib/images/logos/mastermoin1.png contrib/images/logos/mastermoin2.png contrib/images/logos/santa2-moin.png contrib/images/logos/star-moin.png contrib/images/logos/with_text/Logo_MoinMoin.png contrib/images/logos/with_text/Logo_MoinMoin.xcf contrib/images/logos/with_text/Logo_MoinMoin_2.png contrib/interwiki/intermap.txt contrib/wsgi/profiler.py contrib/wsgi/proxy.py contrib/wsgi/raw_wsgi_bench.py contrib/xml/preloaded_items.xml docs/Makefile docs/_static/favicon.ico docs/_static/moinmoin.png docs/admin/backup.rst docs/admin/changes.rst docs/admin/configure.rst docs/admin/install.rst docs/admin/requirements.rst docs/admin/serve.rst docs/admin/upgrade.rst docs/changes/CHANGES docs/changes/CHANGES.html-docbook docs/changes/CHANGES.storage docs/changes/CHANGES.wiki-like-formats-support docs/conf.py docs/devel/development.rst docs/devel/support.rst docs/devel/translate.rst docs/examples/config/logging/logfile docs/examples/config/logging/logfile_debug_auth docs/examples/config/logging/stderr docs/examples/config/snippets/xapian_wikiconfig_snippet docs/examples/config/wikiconfig.py docs/examples/deployment/moin.fcgi docs/examples/deployment/moin.wsgi docs/examples/deployment/test.wsgi docs/index.rst docs/intro/features.rst docs/intro/general.rst docs/intro/glossary.rst docs/intro/license.rst docs/licenses/COPYING docs/licenses/LICENSE.Python docs/licenses/modernized.icons.txt docs/licenses/pikipiki.txt docs/make.bat docs/man/moin.rst docs/todo/2.0-TODO docs/todo/TODO.dom docs/todo/TODO.wiki-like-formats-support docs/user/creolewiki.rst docs/user/docbook.rst docs/user/markups.rst docs/user/mediawiki.rst docs/user/moinwiki.rst docs/user/rest.rst moin moin.spec quickinstall setup.cfg setup.py wiki/data/plugin/__init__.py wikiconfig.py
diffstat 448 files changed, 62997 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,23 @@
+.*\.py[co]$
+^dist/
+^env/
+^moin.egg-info/
+^MoinMoin/_tests/wiki/data/cache/
+^wiki/data/cache/
+^wiki/data/content/
+^wiki/data/userprofiles/
+^wiki/data/trash/
+^instance/
+^wikiconfig_local.*
+^MoinMoin/translations/.*/LC_MESSAGES/messages.mo$
+^docs/_build/
+.coverage
+^.project
+^.pydevproject
+^.settings
+^MANIFEST
+.DS_Store
+.sqlite$
+.orig$
+.rej$
+.~$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,47 @@
+#
+# Makefile for MoinMoin
+#
+
+# location for the wikiconfig.py we use for testing:
+export PYTHONPATH=$(PWD)
+
+all:
+	python setup.py build
+
+dist: clean-devwiki
+	-rm MANIFEST
+	python setup.py sdist
+
+docs:
+	make -C docs html
+
+interwiki:
+	wget -U MoinMoin/Makefile -O contrib/interwiki/intermap.txt "http://master19.moinmo.in/InterWikiMap?action=raw"
+	chmod 664 contrib/interwiki/intermap.txt
+
+check-tabs:
+	@python -c 'import tabnanny ; tabnanny.check("MoinMoin")'
+
+pylint:
+	@pylint --disable-msg=W0142,W0511,W0612,W0613,C0103,C0111,C0302,C0321,C0322 --disable-msg-cat=R MoinMoin
+
+clean: clean-devwiki clean-pyc clean-orig clean-rej
+	-rm -rf build
+
+clean-devwiki:
+	-rm -rf wiki/data/content
+	-rm -rf wiki/data/userprofiles
+	-rm -rf wiki/data/trash
+
+clean-pyc:
+	find . -name "*.pyc" -exec rm -rf "{}" \; 
+
+clean-orig:
+	find . -name "*.orig" -exec rm -rf "{}" \; 
+
+clean-rej:
+	find . -name "*.rej" -exec rm -rf "{}" \; 
+
+.PHONY: all dist docs interwiki check-tabs pylint \
+	clean clean-devwiki clean-pyc clean-orig clean-rej
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,22 @@
+# -*- coding: ascii -*-
+"""
+MoinMoin - a wiki engine in Python.
+
+@copyright: 2000-2006 by Juergen Hermann <jh@web.de>,
+            2002-2011 MoinMoin:ThomasWaldmann
+@license: GNU GPL, see COPYING for details.
+"""
+
+import os
+import sys
+
+project = "MoinMoin"
+
+if sys.hexversion < 0x2060000:
+    sys.exit("%s requires Python 2.6 or greater.\n" % project)
+
+
+from MoinMoin.util.version import Version
+
+version = Version(2, 0, 0, 'alpha')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_template.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,11 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - <short description>
+
+    <what this stuff does ... - verbose enough>
+
+    @copyright: 2011 MoinMoin:YourNameHere
+    @license: GNU GPL, see COPYING for details.
+"""
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,92 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - some common code for testing
+
+    @copyright: 2007 MoinMoin:KarolNowak,
+                2008 MoinMoin:ThomasWaldmann,
+                2008, 2010 MoinMoin:ReimarBauer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import os, shutil
+
+from flask import current_app as app
+from flask import flaskg
+
+
+from MoinMoin import config, security, user
+from MoinMoin.items import Item
+from MoinMoin.util import random_string
+from MoinMoin.storage.error import ItemAlreadyExistsError
+
+# Promoting the test user -------------------------------------------
+# Usually the tests run as anonymous user, but for some stuff, you
+# need more privs...
+
+def become_valid(username=u"ValidUser"):
+    """ modify flaskg.user to make the user valid.
+        Note that a valid user will only be in ACL special group "Known", if
+        we have a user profile for this user as the ACL system will check if
+        there is a userid for this username.
+        Thus, for testing purposes (e.g. if you need delete rights), it is
+        easier to use become_trusted().
+    """
+    flaskg.user.name = username
+    flaskg.user.may.name = username
+    flaskg.user.valid = 1
+
+
+def become_trusted(username=u"TrustedUser"):
+    """ modify flaskg.user to make the user valid and trusted, so it is in acl group Trusted """
+    become_valid(username)
+    flaskg.user.auth_method = app.cfg.auth_methods_trusted[0]
+
+
+def become_superuser(username=u"SuperUser"):
+    """ modify flaskg.user so it is in the superusers list,
+        also make the user valid (see notes in become_valid()),
+        also make the user trusted (and thus in "Trusted" ACL pseudo group).
+
+        Note: being superuser is completely unrelated to ACL rights,
+              especially it is not related to ACL admin rights.
+    """
+    become_trusted(username)
+    if username not in app.cfg.superusers:
+        app.cfg.superusers.append(username)
+
+# Creating and destroying test items --------------------------------
+def update_item(name, revno, meta, data):
+    """ creates or updates an item  """
+    if isinstance(data, unicode):
+        data = data.encode(config.charset)
+    try:
+        item = flaskg.storage.create_item(name)
+    except ItemAlreadyExistsError:
+        item = flaskg.storage.get_item(name)
+
+    rev = item.create_revision(revno)
+    for key, value in meta.items():
+        rev[key] = value
+    if not 'name' in rev:
+        rev['name'] = name
+    if not 'mimetype' in rev:
+        rev['mimetype'] = u'application/octet-stream'
+    rev.write(data)
+    item.commit()
+    return item
+
+def create_random_string_list(length=14, count=10):
+    """ creates a list of random strings """
+    chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+    return [u"%s" % random_string(length, chars) for counter in range(count)]
+
+def nuke_xapian_index():
+    """ completely delete everything in xapian index dir """
+    fpath = app.cfg.xapian_index_dir
+    if os.path.exists(fpath):
+        shutil.rmtree(fpath, True)
+
+def nuke_item(name):
+    """ complete destroys an item """
+    item = Item.create(name)
+    item.destroy()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/_test_template.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - MoinMoin.module_tested Tests
+
+    Module names must start with 'test_' to be included in the tests.
+
+    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
+                2007 MoinMoin:AlexanderSchremmer
+                2009 MoinMoin:ReimarBauer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+# include here the module that you want to test:
+from MoinMoin import module_tested
+
+
+class TestSimpleStuff(object):
+    """ The simplest MoinMoin test class
+
+    Class name must start with 'Test' to be included in
+    the tests.
+
+    See http://codespeak.net/py/dist/test.html for reference.
+    """
+    def testSimplest(self):
+        """ module_tested: test description...
+
+        Function name MUST start with 'test' to be included in the
+        tests.
+        """
+        result = module_tested.some_function('test_value')
+        expected = 'expected value'
+        assert result == expected
+
+
+class TestComplexStuff(object):
+    """ Describe these tests here...
+
+    Some tests may have a list of tests related to this test case. You
+    can add a test by adding another line to this list
+    """
+    _tests = (
+        # description,  test,            expected
+        ('Line break',  '<<BR>>',        '<br>'),
+    )
+
+    from MoinMoin._tests import wikiconfig
+    class Config(wikiconfig.Config):
+        foo = 'bar'  # we want to have this non-default setting
+
+    def setup_class(self):
+        """ Stuff that should be run to init the state of this test class
+        """
+
+    def teardown_class(self):
+        """ Stuff that should run to clean up the state of this test class
+        """
+
+    def testFunction(self):
+        """ module_tested: function should... """
+        for description, test, expected in self._tests:
+            result = self._helper_function(test)
+            assert result == expected
+
+    def _helper_fuction(self, test):
+        """ Some tests needs extra  work to run
+
+        Keep the test non interesting details out of the way.
+        """
+        module_tested.do_this()
+        module_tested.do_that()
+        result = None
+        return result
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/ldap_testbase.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,261 @@
+# -*- coding: utf-8 -*-
+"""
+    LDAPTestBase: LDAP testing support for py.test based unit tests
+
+    Features
+    --------
+
+    * setup_class
+      * automatic creation of a temporary LDAP server environment
+      * automatic creation of a LDAP server process (slapd)
+
+    * teardown_class
+      * LDAP server process will be killed and termination will be waited for
+      * temporary LDAP environment will be removed
+
+    Usage
+    -----
+
+    Write your own test class and derive from LDAPTestBase:
+
+    class TestLdap(LDAPTestBase):
+        def testFunction(self):
+            server_url = self.ldap_env.slapd.url
+            lo = ldap.initialize(server_url)
+            lo.simple_bind_s('', '')
+
+    Notes
+    -----
+
+    On Ubuntu 8.04 there is apparmor imposing some restrictions on /usr/sbin/slapd,
+    so you need to disable apparmor by invoking this as root:
+
+    # /etc/init.d/apparmor stop
+
+    @copyright: 2008 by Thomas Waldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+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, base64
+from StringIO import StringIO
+import signal
+import subprocess
+import hashlib
+
+try:
+    import ldap, ldif, ldap.modlist  # needs python-ldap
+except ImportError:
+    ldap = None
+
+
+def check_environ():
+    """ Check the system environment whether we are able to run.
+        Either return some failure reason if we can't or None if everything
+        looks OK.
+    """
+    if ldap is None:
+        return "You need python-ldap installed to use ldap_testbase."
+    slapd = False
+    try:
+        p = subprocess.Popen([SLAPD_EXECUTABLE, '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        pid = p.pid
+        rc = p.wait()
+        if pid and rc == 1:
+            slapd = True  # it works
+    except OSError, err:
+        import errno
+        if not (err.errno == errno.ENOENT or
+                (err.errno == 3 and os.name == 'nt')):
+            raise
+    if not slapd:
+        return "Can't start %s (see SLAPD_EXECUTABLE)." % SLAPD_EXECUTABLE
+    return None
+
+
+class Slapd(object):
+    """ Manage a slapd process for testing purposes """
+    def __init__(self,
+                 config=None,  # config filename for -f
+                 executable=SLAPD_EXECUTABLE,
+                 debug_flags='', # None,  # for -d stats,acl,args,trace,sync,config
+                 proto='ldap', ip='127.0.0.1', port=3890,  # use -h proto://ip:port
+                 service_name=''  # defaults to -n executable:port, use None to not use -n
+                ):
+        self.executable = executable
+        self.config = config
+        self.debug_flags = debug_flags
+        self.proto = proto
+        self.ip = ip
+        self.port = port
+        self.url = '%s://%s:%d' % (proto, ip, port) # can be used for ldap.initialize() call
+        if service_name == '':
+            self.service_name = '%s:%d' % (executable, port)
+        else:
+            self.service_name = service_name
+
+    def start(self, timeout=0):
+        """ start a slapd process and optionally wait up to timeout seconds until it responds """
+        args = [self.executable, '-h', self.url, ]
+        if self.config is not None:
+            args.extend(['-f', self.config])
+        if self.debug_flags is not None:
+            args.extend(['-d', self.debug_flags])
+        if self.service_name:
+            args.extend(['-n', self.service_name])
+        self.process = subprocess.Popen(args)
+        started = None
+        if timeout:
+            lo = ldap.initialize(self.url)
+            ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+            started = False
+            wait_until = time.time() + timeout
+            while time.time() < wait_until:
+                try:
+                    lo.simple_bind_s('', '')
+                    started = True
+                except ldap.SERVER_DOWN, err:
+                    time.sleep(0.1)
+                else:
+                    break
+        return started
+
+    def stop(self):
+        """ stop this slapd process and wait until it has terminated """
+        pid = self.process.pid
+        os.kill(pid, signal.SIGTERM)
+        os.waitpid(pid, 0)
+
+
+class LdapEnvironment(object):
+    """ Manage a (temporary) environment for running a slapd in it """
+
+    # default DB_CONFIG bdb configuration file contents
+    DB_CONFIG = """\
+# STRANGE: if i use those settings, after the test slapd goes to 100% and doesn't terminate on SIGTERM
+# Set the database in memory cache size.
+#set_cachesize 0 10000000 1
+
+# Set log values.
+#set_lg_regionmax 262144
+#set_lg_bsize 262144
+#set_lg_max 10485760
+
+#set_tas_spins 0
+"""
+
+    def __init__(self,
+                 basedn,
+                 rootdn, rootpw,
+                 instance=0,  # use different values when running multiple LdapEnvironments
+                 schema_dir='/etc/ldap/schema',  # directory with schemas
+                 coding='utf-8',  # coding used for config files
+                 timeout=10,  # how long to wait for slapd starting [s]
+                ):
+        self.basedn = basedn
+        self.rootdn = rootdn
+        self.rootpw = rootpw
+        self.instance = instance
+        self.schema_dir = schema_dir
+        self.coding = coding
+        self.ldap_dir = None
+        self.slapd_conf = None
+        self.timeout = timeout
+
+    def create_env(self, slapd_config, db_config=DB_CONFIG):
+        """ create a temporary LDAP server environment in a temp. directory,
+            including writing a slapd.conf (see configure_slapd) and a
+            DB_CONFIG there.
+        """
+        # create directories
+        self.ldap_dir = tempfile.mkdtemp(prefix='LdapEnvironment-%d.' % self.instance)
+        self.ldap_db_dir = os.path.join(self.ldap_dir, 'db')
+        os.mkdir(self.ldap_db_dir)
+
+        # create DB_CONFIG for bdb backend
+        db_config_fname = os.path.join(self.ldap_db_dir, 'DB_CONFIG')
+        f = open(db_config_fname, 'w')
+        f.write(db_config)
+        f.close()
+
+        rootpw = '{MD5}' + base64.b64encode(hashlib.new('md5', self.rootpw).digest())
+
+        # create slapd.conf from content template in slapd_config
+        slapd_config = slapd_config % {
+            'ldap_dir': self.ldap_dir,
+            'ldap_db_dir': self.ldap_db_dir,
+            'schema_dir': self.schema_dir,
+            'basedn': self.basedn,
+            'rootdn': self.rootdn,
+            'rootpw': rootpw,
+        }
+        if isinstance(slapd_config, unicode):
+            slapd_config = slapd_config.encode(self.coding)
+        self.slapd_conf = os.path.join(self.ldap_dir, "slapd.conf")
+        f = open(self.slapd_conf, 'w')
+        f.write(slapd_config)
+        f.close()
+
+    def start_slapd(self):
+        """ start a slapd and optionally wait until it talks with us """
+        self.slapd = Slapd(config=self.slapd_conf, port=3890+self.instance)
+        started = self.slapd.start(timeout=self.timeout)
+        return started
+
+    def load_directory(self, ldif_content):
+        """ load the directory with the ldif_content (str) """
+        lo = ldap.initialize(self.slapd.url)
+        ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+        lo.simple_bind_s(self.rootdn, self.rootpw)
+
+        class LDIFLoader(ldif.LDIFParser):
+            def handle(self, dn, entry):
+                lo.add_s(dn, ldap.modlist.addModlist(entry))
+
+        loader = LDIFLoader(StringIO(ldif_content))
+        loader.parse()
+
+    def stop_slapd(self):
+        """ stop a slapd """
+        self.slapd.stop()
+
+    def destroy_env(self):
+        """ remove the temporary LDAP server environment """
+        shutil.rmtree(self.ldap_dir)
+
+try:
+    import py.test
+
+    class LDAPTstBase:
+        """ Test base class for py.test based tests which need a LDAP server to talk to.
+
+            Inherit your test class from this base class to test LDAP stuff.
+        """
+
+        # You MUST define these in your derived class:
+        slapd_config = None  # a string with your slapd.conf template
+        ldif_content = None  # a string with your ldif contents
+        basedn = None  # your base DN
+        rootdn = None  # root DN
+        rootpw = None  # root password
+
+        def setup_class(self):
+            """ Create LDAP server environment, start slapd """
+            self.ldap_env = LdapEnvironment(self.basedn, self.rootdn, self.rootpw)
+            self.ldap_env.create_env(slapd_config=self.slapd_config)
+            started = self.ldap_env.start_slapd()
+            if not started:
+                py.test.skip("Failed to start %s process, please see your syslog / log files"
+                             " (and check if stopping apparmor helps, in case you use it)." % SLAPD_EXECUTABLE)
+            self.ldap_env.load_directory(ldif_content=self.ldif_content)
+
+        def teardown_class(self):
+            """ Stop slapd, remove LDAP server environment """
+            self.ldap_env.stop_slapd()
+            self.ldap_env.destroy_env()
+
+except ImportError:
+    pass  # obviously py.test not in use
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/ldap_testdata.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,111 @@
+BASEDN = "ou=testing,dc=example,dc=org"
+ROOTDN = "cn=root,%s" % BASEDN
+ROOTPW = "secret"
+
+SLAPD_CONFIG = """\
+# See slapd.conf(5) for details on configuration options.
+
+include		%(schema_dir)s/core.schema
+include		%(schema_dir)s/cosine.schema
+include		%(schema_dir)s/inetorgperson.schema
+#include	%(schema_dir)s/misc.schema
+
+moduleload	back_bdb.la
+
+threads 2
+
+# Global access control ###############################################
+
+# Root DSE: allow anyone to read it
+access to dn.base="" by * read
+# Subschema (sub)entry DSE: allow anyone to read it
+access to dn.base="cn=Subschema" by * read
+
+# we don't need restrictive ACLs for tests:
+access to * by * read
+
+allow bind_anon_dn
+
+# Test-Datenbank ou=testing,dc=example,dc=org ################
+
+database	bdb
+
+directory	%(ldap_db_dir)s
+suffix		"%(basedn)s"
+rootdn		"%(rootdn)s"
+rootpw		%(rootpw)s
+lastmod		on
+
+index 		uid eq
+
+checkpoint 200 5
+
+# Entries to cache in memory
+cachesize 500
+# Search results to cache in memory
+idlcachesize 50
+
+sizelimit	-1
+"""
+
+LDIF_CONTENT = """\
+########################################################################
+# regression testing
+########################################################################
+version: 1
+
+dn: ou=testing,dc=example,dc=org
+objectClass: organizationalUnit
+ou: testing
+
+dn: ou=Groups,ou=testing,dc=example,dc=org
+objectClass: organizationalUnit
+ou: Groups
+
+dn: ou=Users,ou=testing,dc=example,dc=org
+objectClass: organizationalUnit
+ou: Users
+
+dn: ou=Unit A,ou=Users,ou=testing,dc=example,dc=org
+objectClass: organizationalUnit
+ou: Unit A
+
+dn: ou=Unit B,ou=Users,ou=testing,dc=example,dc=org
+objectClass: organizationalUnit
+ou: Unit B
+
+dn: uid=usera,ou=Unit A,ou=Users,ou=testing,dc=example,dc=org
+objectClass: account
+objectClass: simpleSecurityObject
+uid: 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
+# 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
+member: cn=dummy
+member: uid=usera,ou=Unit A,ou=Users,ou=testing,dc=example,dc=org
+objectClass: groupOfNames
+
+dn: cn=Group B,ou=Groups,ou=testing,dc=example,dc=org
+cn: Group B
+objectClass: groupOfUniqueNames
+uniqueMember: cn=dummy
+uniqueMember: uid=userb,ou=Unit B,ou=Users,ou=testing,dc=example,dc=org
+
+dn: cn=Group C,ou=Groups,ou=testing,dc=example,dc=org
+cn: Group C
+description: Nested group!
+member: cn=dummy
+member: cn=Group A,ou=Groups,ou=testing,dc=example,dc=org
+objectClass: groupOfNames
+"""
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/maketestwiki.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,56 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - make a test wiki
+
+Usage:
+
+    maketestwiki.py
+
+@copyright: 2005 by Thomas Waldmann
+@license: GNU GPL, see COPYING for details.
+"""
+
+import os, sys, shutil, errno
+import tarfile
+
+filename = globals().get("__file__") or sys.argv[0]
+moinpath = os.path.abspath(os.path.join(os.path.dirname(filename), os.pardir, os.pardir))
+
+WIKI = os.path.abspath(os.path.join(moinpath, 'tests', 'wiki'))
+SHARE = os.path.abspath(os.path.join(moinpath, 'wiki'))
+
+
+def removeTestWiki():
+    print 'removing old wiki ...'
+    dir = 'data'
+    try:
+        shutil.rmtree(os.path.join(WIKI, dir))
+    except OSError, err:
+        if not (err.errno == errno.ENOENT or
+                (err.errno == 3 and os.name == 'nt')):
+            raise
+
+
+def copyData():
+    print 'copying data ...'
+    src = os.path.join(SHARE, 'data')
+    dst = os.path.join(WIKI, 'data')
+    shutil.copytree(src, dst)
+
+
+def run(skip_if_existing=False):
+    try:
+        os.makedirs(WIKI)
+    except OSError, e:
+        if e.errno != errno.EEXIST:
+            raise
+
+    if skip_if_existing and os.path.exists(os.path.join(WIKI, 'data')):
+        return
+    removeTestWiki()
+    copyData()
+
+if __name__ == '__main__':
+    sys.path.insert(0, moinpath)
+    run()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/pep8.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,916 @@
+#!/usr/bin/python
+# pep8.py - Check Python source code formatting, according to PEP 8
+# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+"""
+Check Python source code formatting, according to PEP 8:
+http://www.python.org/dev/peps/pep-0008/
+
+For usage and a list of options, try this:
+$ python pep8.py -h
+
+This program and its regression test suite live here:
+http://svn.browsershots.org/trunk/devtools/pep8/
+http://trac.browsershots.org/browser/trunk/devtools/pep8/
+
+Groups of errors and warnings:
+E errors
+W warnings
+100 indentation
+200 whitespace
+300 blank lines
+400 imports
+500 line length
+600 deprecation
+700 statements
+
+You can add checks to this program by writing plugins. Each plugin is
+a simple function that is called for each line of source code, either
+physical or logical.
+
+Physical line:
+- Raw line of text from the input file.
+
+Logical line:
+- Multi-line statements converted to a single line.
+- Stripped left and right.
+- Contents of strings replaced with 'xxx' of same length.
+- Comments removed.
+
+The check function requests physical or logical lines by the name of
+the first argument:
+
+def maximum_line_length(physical_line)
+def extraneous_whitespace(logical_line)
+def blank_lines(logical_line, blank_lines, indent_level, line_number)
+
+The last example above demonstrates how check plugins can request
+additional information with extra arguments. All attributes of the
+Checker object are available. Some examples:
+
+lines: a list of the raw lines from the input file
+tokens: the tokens that contribute to this logical line
+line_number: line number in the input file
+blank_lines: blank lines before this one
+indent_char: first indentation character in this file (' ' or '\t')
+indent_level: indentation (with tabs expanded to multiples of 8)
+previous_indent_level: indentation on previous line
+previous_logical: previous logical line
+
+The docstring of each check function shall be the relevant part of
+text from PEP 8. It is printed if the user enables --show-pep8.
+
+"""
+
+import os
+import sys
+import re
+import time
+import inspect
+import tokenize
+from optparse import OptionParser
+from keyword import iskeyword
+from fnmatch import fnmatch
+
+__version__ = '0.2.0'
+__revision__ = '$Rev$'
+
+default_exclude = '.svn,CVS,*.pyc,*.pyo'
+
+indent_match = re.compile(r'([ \t]*)').match
+raise_comma_match = re.compile(r'raise\s+\w+\s*(,)').match
+equals_boolean_search = re.compile(r'([!=]=\s*(True|False))|((True|False)\s*[!=]=)').search
+equals_None_search = re.compile(r'([!=]=\s*None)|(None\s*[!=]=)').search
+
+not_one_ws_around_operators_match = re.compile(r'^[^\(\[]+[^\s](\+|\-|\*|/|%|\^|&|\||=|<|>|>>|<<|\+=|\-=|\*=|/=|%=|\^=|&=|\|=|==|<=|>=|>>=|<<=|!=|<>)[^\s][^\)\]]+$').match
+
+operators = """
++  -  *  /  %  ^  &  |  =  <  >  >>  <<
++= -= *= /= %= ^= &= |= == <= >= >>= <<=
+!= <> :
+in is or not and
+""".split()
+
+options = None
+args = None
+
+
+##############################################################################
+# Plugins (check functions) for physical lines
+##############################################################################
+
+
+def tabs_or_spaces(physical_line, indent_char):
+    """
+    Never mix tabs and spaces.
+
+    The most popular way of indenting Python is with spaces only.  The
+    second-most popular way is with tabs only.  Code indented with a mixture
+    of tabs and spaces should be converted to using spaces exclusively.  When
+    invoking the Python command line interpreter with the -t option, it issues
+    warnings about code that illegally mixes tabs and spaces.  When using -tt
+    these warnings become errors.  These options are highly recommended!
+    """
+    indent = indent_match(physical_line).group(1)
+    for offset, char in enumerate(indent):
+        if char != indent_char:
+            return offset, "E101 indentation contains mixed spaces and tabs"
+
+
+def tabs_obsolete(physical_line):
+    """
+    For new projects, spaces-only are strongly recommended over tabs.  Most
+    editors have features that make this easy to do.
+    """
+    indent = indent_match(physical_line).group(1)
+    if indent.count('\t'):
+        return indent.index('\t'), "W191 indentation contains tabs"
+
+
+def trailing_whitespace(physical_line):
+    """
+    JCR: Trailing whitespace is superfluous.
+    """
+    physical_line = physical_line.rstrip('\n') # chr(10), newline
+    physical_line = physical_line.rstrip('\r') # chr(13), carriage return
+    physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L
+    stripped = physical_line.rstrip()
+    if physical_line != stripped:
+        return len(stripped), "W291 trailing whitespace"
+
+
+def trailing_blank_lines(physical_line, lines, line_number):
+    """
+    JCR: Trailing blank lines are superfluous.
+    """
+    if physical_line.strip() == '' and line_number == len(lines):
+        return 0, "W391 blank line at end of file"
+
+
+def missing_newline(physical_line):
+    """
+    JCR: The last line should have a newline.
+    """
+    if physical_line.rstrip() == physical_line:
+        return len(physical_line), "W292 no newline at end of file"
+
+
+def maximum_line_length(physical_line):
+    """
+    Limit all lines to a maximum of 79 characters.
+
+    There are still many devices around that are limited to 80 character
+    lines; plus, limiting windows to 80 characters makes it possible to have
+    several windows side-by-side.  The default wrapping on such devices looks
+    ugly.  Therefore, please limit all lines to a maximum of 79 characters.
+    For flowing long blocks of text (docstrings or comments), limiting the
+    length to 72 characters is recommended.
+    """
+    length = len(physical_line.rstrip())
+    if length > 79:
+        return 79, "E501 line too long (%d characters)" % length
+
+
+def crlf_lines(physical_line):
+    """
+    Line contains CR (e.g. as a CRLF line ending).
+
+    Many free software projects have a strong focus on POSIX platforms (like
+    Linux, *BSD, Unix, Mac OS X, etc.) and they all use LF-only line endings.
+    Only Win32 platform uses CRLF line endings.
+    So if you have a Win32-only source code using CRLF line endings, you might
+    want to exclude this test.
+    """
+    pos = physical_line.find('\r')
+    if pos >= 0:
+        return pos, "W293 line contains CR char(s)"
+
+
+##############################################################################
+# Plugins (check functions) for logical lines
+##############################################################################
+
+
+def blank_lines(logical_line, blank_lines, indent_level, line_number,
+                previous_logical):
+    """
+    Separate top-level function and class definitions with two blank lines.
+
+    Method definitions inside a class are separated by a single blank line.
+
+    Extra blank lines may be used (sparingly) to separate groups of related
+    functions.  Blank lines may be omitted between a bunch of related
+    one-liners (e.g. a set of dummy implementations).
+
+    Use blank lines in functions, sparingly, to indicate logical sections.
+    """
+    if line_number == 1:
+        return # Don't expect blank lines before the first line
+    if previous_logical.startswith('@'):
+        return # Don't expect blank lines after function decorator
+    if (logical_line.startswith('def ') or
+        logical_line.startswith('class ') or
+        logical_line.startswith('@')):
+        if indent_level > 0 and blank_lines != 1:
+            return 0, "E301 expected 1 blank line, found %d" % blank_lines
+        if indent_level == 0 and blank_lines != 2:
+            return 0, "E302 expected 2 blank lines, found %d" % blank_lines
+    if blank_lines > 2:
+        return 0, "E303 too many blank lines (%d)" % blank_lines
+
+
+def extraneous_whitespace(logical_line):
+    """
+    Avoid extraneous whitespace in the following situations:
+
+    - Immediately inside parentheses, brackets or braces.
+
+    - Immediately before a comma, semicolon, or colon.
+    """
+    line = logical_line
+    for char in '([{':
+        found = line.find(char + ' ')
+        if found > -1:
+            return found + 1, "E201 whitespace after '%s'" % char
+    for char in '}])':
+        found = line.find(' ' + char)
+        if found > -1 and line[found - 1] != ',':
+            return found, "E202 whitespace before '%s'" % char
+    for char in ',;:':
+        found = line.find(' ' + char)
+        if found > -1:
+            return found, "E203 whitespace before '%s'" % char
+
+
+def missing_whitespace(logical_line):
+    """
+    JCR: Each comma, semicolon or colon should be followed by whitespace.
+    """
+    line = logical_line
+    for index in range(len(line) - 1):
+        char = line[index]
+        if char in ',;:' and line[index + 1] != ' ':
+            before = line[:index]
+            if char == ':' and before.count('[') > before.count(']'):
+                continue # Slice syntax, no space required
+            return index, "E231 missing whitespace after '%s'" % char
+
+
+def indentation(logical_line, previous_logical, indent_char,
+                indent_level, previous_indent_level):
+    """
+    Use 4 spaces per indentation level.
+
+    For really old code that you don't want to mess up, you can continue to
+    use 8-space tabs.
+    """
+    if indent_char == ' ' and indent_level % 4:
+        return 0, "E111 indentation is not a multiple of four"
+    indent_expect = previous_logical.endswith(':')
+    if indent_expect and indent_level <= previous_indent_level:
+        return 0, "E112 expected an indented block"
+    if indent_level > previous_indent_level and not indent_expect:
+        return 0, "E113 unexpected indentation"
+
+
+def whitespace_before_parameters(logical_line, tokens):
+    """
+    Avoid extraneous whitespace in the following situations:
+
+    - Immediately before the open parenthesis that starts the argument
+      list of a function call.
+
+    - Immediately before the open parenthesis that starts an indexing or
+      slicing.
+    """
+    prev_type = tokens[0][0]
+    prev_text = tokens[0][1]
+    prev_end = tokens[0][3]
+    for index in range(1, len(tokens)):
+        token_type, text, start, end, line = tokens[index]
+        if (token_type == tokenize.OP and
+            text in '([' and
+            start != prev_end and
+            prev_type == tokenize.NAME and
+            (index < 2 or tokens[index - 2][1] != 'class') and
+            (not iskeyword(prev_text))):
+            return prev_end, "E211 whitespace before '%s'" % text
+        prev_type = token_type
+        prev_text = text
+        prev_end = end
+
+
+def extra_whitespace_around_operator(logical_line):
+    """
+    Avoid extraneous whitespace in the following situations:
+
+    - More than one space around an assignment (or other) operator to
+      align it with another.
+    """
+    line = logical_line
+    for operator in operators:
+        found = line.find('  ' + operator)
+        if found > -1:
+            return found, "E221 multiple spaces before operator"
+        found = line.find(operator + '  ')
+        if found > -1:
+            return found, "E222 multiple spaces after operator"
+        found = line.find('\t' + operator)
+        if found > -1:
+            return found, "E223 tab before operator"
+        found = line.find(operator + '\t')
+        if found > -1:
+            return found, "E224 tab after operator"
+
+
+def whitespace_around_operator(logical_line):
+    """
+    Have exactly 1 space left and right of the operator.
+    """
+    match = not_one_ws_around_operators_match(logical_line)
+    if match and not 'lambda' in logical_line:
+        return match.start(1), "E225 operators shall be surrounded by a single space on each side %s" % logical_line
+
+
+def whitespace_around_comma(logical_line):
+    """
+    Avoid extraneous whitespace in the following situations:
+
+    - More than one space around an assignment (or other) operator to
+      align it with another.
+
+    JCR: This should also be applied around comma etc.
+    """
+    line = logical_line
+    for separator in ',;:':
+        found = line.find(separator + '  ')
+        if found > -1:
+            return found + 1, "E241 multiple spaces after '%s'" % separator
+        found = line.find(separator + '\t')
+        if found > -1:
+            return found + 1, "E242 tab after '%s'" % separator
+
+
+def imports_on_separate_lines(logical_line):
+    """
+    Imports should usually be on separate lines.
+    """
+    line = logical_line
+    if line.startswith('import '):
+        found = line.find(',')
+        if found > -1:
+            return found, "E401 multiple imports on one line"
+
+
+def compound_statements(logical_line):
+    """
+    Compound statements (multiple statements on the same line) are
+    generally discouraged.
+    """
+    line = logical_line
+    found = line.find(':')
+    if -1 < found < len(line) - 1:
+        before = line[:found]
+        if (before.count('{') <= before.count('}') and # {'a': 1} (dict)
+            before.count('[') <= before.count(']') and # [1:2] (slice)
+            not re.search(r'\blambda\b', before)):     # lambda x: x
+            return found, "E701 multiple statements on one line (colon)"
+    found = line.find(';')
+    if -1 < found:
+        return found, "E702 multiple statements on one line (semicolon)"
+
+
+def python_3000_has_key(logical_line):
+    """
+    The {}.has_key() method will be removed in the future version of
+    Python. Use the 'in' operation instead, like:
+    d = {"a": 1, "b": 2}
+    if "b" in d:
+        print d["b"]
+    """
+    pos = logical_line.find('.has_key(')
+    if pos > -1:
+        return pos, "W601 .has_key() is deprecated, use 'in'"
+
+
+def python_3000_raise_comma(logical_line):
+    """
+    When raising an exception, use "raise ValueError('message')"
+    instead of the older form "raise ValueError, 'message'".
+
+    The paren-using form is preferred because when the exception arguments
+    are long or include string formatting, you don't need to use line
+    continuation characters thanks to the containing parentheses.  The older
+    form will be removed in Python 3000.
+    """
+    match = raise_comma_match(logical_line)
+    if match:
+        return match.start(1), "W602 deprecated form of raising exception"
+
+
+def dumb_equals_boolean(logical_line):
+    """
+    Using "if x == True:" or "if x == False:" is wrong in any case:
+
+    First if you already have a boolean, you don't need to compare it to
+    another boolean. Just use "if x:" or "if not x:".
+
+    Second, even if you have some sort of "tristate logic", not only using
+    True/False, but other values, then you want to use "if x is True:" or
+    "if x is False:" because there is exactly one True and one False object.
+    """
+    match = equals_boolean_search(logical_line)
+    if match:
+        return match.start(1), "E798 don't use 'x == <boolean>', but just 'x' or 'not x' or 'x is <boolean>'"
+
+
+def dumb_equals_None(logical_line):
+    """
+    Using "if x == None:" is wrong in any case:
+
+    You either want to use "if x is None:" (there is only 1 None object) or -
+    in some simple cases - just "if not x:".
+    """
+    match = equals_None_search(logical_line)
+    if match:
+        return match.start(1), "E799 don't use 'x == None', but just 'x is None' or 'not x'"
+
+
+##############################################################################
+# Helper functions
+##############################################################################
+
+
+def expand_indent(line):
+    """
+    Return the amount of indentation.
+    Tabs are expanded to the next multiple of 8.
+
+    >>> expand_indent('    ')
+    4
+    >>> expand_indent('\\t')
+    8
+    >>> expand_indent('    \\t')
+    8
+    >>> expand_indent('       \\t')
+    8
+    >>> expand_indent('        \\t')
+    16
+    """
+    result = 0
+    for char in line:
+        if char == '\t':
+            result = result / 8 * 8 + 8
+        elif char == ' ':
+            result += 1
+        else:
+            break
+    return result
+
+
+##############################################################################
+# Framework to run all checks
+##############################################################################
+
+
+def message(text):
+    """Print a message."""
+    # print >> sys.stderr, options.prog + ': ' + text
+    # print >> sys.stderr, text
+    print text
+
+
+def find_checks(argument_name):
+    """
+    Find all globally visible functions where the first argument name
+    starts with argument_name.
+    """
+    checks = []
+    function_type = type(find_checks)
+    for name, function in globals().iteritems():
+        if type(function) is function_type:
+            args = inspect.getargspec(function)[0]
+            if len(args) >= 1 and args[0].startswith(argument_name):
+                checks.append((name, function, args))
+    checks.sort()
+    return checks
+
+
+def mute_string(text):
+    """
+    Replace contents with 'xxx' to prevent syntax matching.
+
+    >>> mute_string('"abc"')
+    '"xxx"'
+    >>> mute_string("'''abc'''")
+    "'''xxx'''"
+    >>> mute_string("r'abc'")
+    "r'xxx'"
+    """
+    start = 1
+    end = len(text) - 1
+    # String modifiers (e.g. u or r)
+    if text.endswith('"'):
+        start += text.index('"')
+    elif text.endswith("'"):
+        start += text.index("'")
+    # Triple quotes
+    if text.endswith('"""') or text.endswith("'''"):
+        start += 2
+        end -= 2
+    return text[:start] + 'x' * (end - start) + text[end:]
+
+
+class Checker:
+    """
+    Load a Python source file, tokenize it, check coding style.
+    """
+
+    def __init__(self, filename):
+        self.filename = filename
+        self.lines = file(filename, 'rb').readlines()
+        self.physical_checks = find_checks('physical_line')
+        self.logical_checks = find_checks('logical_line')
+        options.counters['physical lines'] = \
+            options.counters.get('physical lines', 0) + len(self.lines)
+
+    def readline(self):
+        """
+        Get the next line from the input buffer.
+        """
+        self.line_number += 1
+        if self.line_number > len(self.lines):
+            return ''
+        return self.lines[self.line_number - 1]
+
+    def readline_check_physical(self):
+        """
+        Check and return the next physical line. This method can be
+        used to feed tokenize.generate_tokens.
+        """
+        line = self.readline()
+        if line:
+            self.check_physical(line)
+        return line
+
+    def run_check(self, check, argument_names):
+        """
+        Run a check plugin.
+        """
+        arguments = []
+        for name in argument_names:
+            arguments.append(getattr(self, name))
+        return check(*arguments)
+
+    def check_physical(self, line):
+        """
+        Run all physical checks on a raw input line.
+        """
+        self.physical_line = line
+        if self.indent_char is None and len(line) and line[0] in ' \t':
+            self.indent_char = line[0]
+        for name, check, argument_names in self.physical_checks:
+            result = self.run_check(check, argument_names)
+            if result is not None:
+                offset, text = result
+                self.report_error(self.line_number, offset, text, check)
+
+    def build_tokens_line(self):
+        """
+        Build a logical line from tokens.
+        """
+        self.mapping = []
+        logical = []
+        length = 0
+        previous = None
+        for token in self.tokens:
+            token_type, text = token[0:2]
+            if token_type in (tokenize.COMMENT, tokenize.NL,
+                              tokenize.INDENT, tokenize.DEDENT,
+                              tokenize.NEWLINE):
+                continue
+            if token_type == tokenize.STRING:
+                text = mute_string(text)
+            if previous:
+                end_line, end = previous[3]
+                start_line, start = token[2]
+                if end_line != start_line: # different row
+                    if self.lines[end_line - 1][end - 1] not in '{[(':
+                        logical.append(' ')
+                        length += 1
+                elif end != start: # different column
+                    fill = self.lines[end_line - 1][end:start]
+                    logical.append(fill)
+                    length += len(fill)
+            self.mapping.append((length, token))
+            logical.append(text)
+            length += len(text)
+            previous = token
+        self.logical_line = ''.join(logical)
+        assert self.logical_line.lstrip() == self.logical_line
+        assert self.logical_line.rstrip() == self.logical_line
+
+    def check_logical(self):
+        """
+        Build a line from tokens and run all logical checks on it.
+        """
+        options.counters['logical lines'] = \
+            options.counters.get('logical lines', 0) + 1
+        self.build_tokens_line()
+        first_line = self.lines[self.mapping[0][1][2][0] - 1]
+        indent = first_line[:self.mapping[0][1][2][1]]
+        self.previous_indent_level = self.indent_level
+        self.indent_level = expand_indent(indent)
+        if options.verbose >= 2:
+            print self.logical_line[:80].rstrip()
+        for name, check, argument_names in self.logical_checks:
+            if options.verbose >= 3:
+                print '   ', name
+            result = self.run_check(check, argument_names)
+            if result is not None:
+                offset, text = result
+                if type(offset) is tuple:
+                    original_number, original_offset = offset
+                else:
+                    for token_offset, token in self.mapping:
+                        if offset >= token_offset:
+                            original_number = token[2][0]
+                            original_offset = (token[2][1]
+                                               + offset - token_offset)
+                self.report_error(original_number, original_offset,
+                                  text, check)
+        self.previous_logical = self.logical_line
+
+    def check_all(self):
+        """
+        Run all checks on the input file.
+        """
+        self.file_errors = 0
+        self.line_number = 0
+        self.indent_char = None
+        self.indent_level = 0
+        self.previous_logical = ''
+        self.blank_lines = 0
+        self.tokens = []
+        parens = 0
+        for token in tokenize.generate_tokens(self.readline_check_physical):
+            # print tokenize.tok_name[token[0]], repr(token)
+            self.tokens.append(token)
+            token_type, text = token[0:2]
+            if token_type == tokenize.OP and text in '([{':
+                parens += 1
+            if token_type == tokenize.OP and text in '}])':
+                parens -= 1
+            if token_type == tokenize.NEWLINE and not parens:
+                self.check_logical()
+                self.blank_lines = 0
+                self.tokens = []
+            if token_type == tokenize.NL and not parens:
+                self.blank_lines += 1
+                self.tokens = []
+            if token_type == tokenize.COMMENT:
+                self.blank_lines = 0
+        return self.file_errors
+
+    def report_error(self, line_number, offset, text, check):
+        """
+        Report an error, according to options.
+        """
+        if options.quiet == 1 and not self.file_errors:
+            message(self.filename)
+        code = text[:4]
+        if ignore_code(code):
+            return
+        self.file_errors += 1
+        options.counters[code] = options.counters.get(code, 0) + 1
+        options.messages[code] = text[5:]
+        if options.quiet:
+            return
+        if options.testsuite:
+            base = os.path.basename(self.filename)[:4]
+            if base == code:
+                return
+            if base[0] == 'E' and code[0] == 'W':
+                return
+        if options.counters[code] == 1 or options.repeat:
+            message("%s:%s:%d: %s" %
+                    (self.filename, line_number, offset + 1, text))
+            if options.show_source:
+                line = self.lines[line_number - 1]
+                message(line.rstrip())
+                message(' ' * offset + '^')
+            if options.show_pep8:
+                message(check.__doc__.lstrip('\n').rstrip())
+
+
+def input_file(filename):
+    """
+    Run all checks on a Python source file.
+    """
+    if excluded(filename) or not filename_match(filename):
+        return {}
+    if options.verbose:
+        message('checking ' + filename)
+    options.counters['files'] = options.counters.get('files', 0) + 1
+    errors = Checker(filename).check_all()
+    if options.testsuite and not errors:
+        message("%s: %s" % (filename, "no errors found"))
+
+
+def input_dir(dirname):
+    """
+    Check all Python source files in this directory and all subdirectories.
+    """
+    dirname = dirname.rstrip('/')
+    if excluded(dirname):
+        return
+    for root, dirs, files in os.walk(dirname):
+        if options.verbose:
+            message('directory ' + root)
+        options.counters['directories'] = \
+            options.counters.get('directories', 0) + 1
+        dirs.sort()
+        for subdir in dirs:
+            if excluded(subdir):
+                dirs.remove(subdir)
+        files.sort()
+        for filename in files:
+            input_file(os.path.join(root, filename))
+
+
+def excluded(filename):
+    """
+    Check if options.exclude contains a pattern that matches filename.
+    """
+    basename = os.path.basename(filename)
+    for pattern in options.exclude:
+        if fnmatch(basename, pattern):
+            # print basename, 'excluded because it matches', pattern
+            return True
+
+
+def filename_match(filename):
+    """
+    Check if options.filename contains a pattern that matches filename.
+    If options.filename is unspecified, this always returns True.
+    """
+    if not options.filename:
+        return True
+    for pattern in options.filename:
+        if fnmatch(filename, pattern):
+            return True
+
+
+def ignore_code(code):
+    """
+    Check if options.ignore contains a prefix of the error code.
+    """
+    for ignore in options.ignore:
+        if code.startswith(ignore):
+            return True
+
+
+def get_error_statistics():
+    """Get error statistics."""
+    return get_statistics("E")
+
+
+def get_warning_statistics():
+    """Get warning statistics."""
+    return get_statistics("W")
+
+
+def get_statistics(prefix=''):
+    """
+    Get statistics for message codes that start with the prefix.
+
+    prefix='' matches all errors and warnings
+    prefix='E' matches all errors
+    prefix='W' matches all warnings
+    prefix='E4' matches all errors that have to do with imports
+    """
+    stats = []
+    keys = options.messages.keys()
+    keys.sort()
+    for key in keys:
+        if key.startswith(prefix):
+            stats.append('%-7s %s %s' %
+                         (options.counters[key], key, options.messages[key]))
+    return stats
+
+
+def print_statistics(prefix=''):
+    """Print overall statistics (number of errors and warnings)."""
+    for line in get_statistics(prefix):
+        print line
+
+
+def print_benchmark(elapsed):
+    """
+    Print benchmark numbers.
+    """
+    print '%-7.2f %s' % (elapsed, 'seconds elapsed')
+    keys = ['directories', 'files',
+            'logical lines', 'physical lines']
+    for key in keys:
+        if key in options.counters:
+            print '%-7d %s per second (%d total)' % (
+                options.counters[key] / elapsed, key,
+                options.counters[key])
+
+
+def process_options(arglist=None):
+    """
+    Process options passed either via arglist or via command line args.
+    """
+    global options, args
+    usage = "%prog [options] input ..."
+    parser = OptionParser(usage)
+    parser.add_option('-v', '--verbose', default=0, action='count',
+                      help="print status messages, or debug with -vv")
+    parser.add_option('-q', '--quiet', default=0, action='count',
+                      help="report only file names, or nothing with -qq")
+    parser.add_option('--exclude', metavar='patterns', default=default_exclude,
+                      help="skip matches (default %s)" % default_exclude)
+    parser.add_option('--filename', metavar='patterns',
+                      help="only check matching files (e.g. *.py)")
+    parser.add_option('--ignore', metavar='errors', default='',
+                      help="skip errors and warnings (e.g. E4,W)")
+    parser.add_option('--repeat', action='store_true',
+                      help="show all occurrences of the same error")
+    parser.add_option('--show-source', action='store_true',
+                      help="show source code for each error")
+    parser.add_option('--show-pep8', action='store_true',
+                      help="show text of PEP 8 for each error")
+    parser.add_option('--statistics', action='store_true',
+                      help="count errors and warnings")
+    parser.add_option('--benchmark', action='store_true',
+                      help="measure processing speed")
+    parser.add_option('--testsuite', metavar='dir',
+                      help="run regression tests from dir")
+    parser.add_option('--doctest', action='store_true',
+                      help="run doctest on myself")
+    options, args = parser.parse_args(arglist)
+    if options.testsuite:
+        args.append(options.testsuite)
+    if len(args) == 0:
+        parser.error('input not specified')
+    options.prog = os.path.basename(sys.argv[0])
+    options.exclude = options.exclude.split(',')
+    for index in range(len(options.exclude)):
+        options.exclude[index] = options.exclude[index].rstrip('/')
+    if options.filename:
+        options.filename = options.filename.split(',')
+    if options.ignore:
+        options.ignore = options.ignore.split(',')
+    else:
+        options.ignore = []
+    options.counters = {}
+    options.messages = {}
+
+    return options, args
+
+
+def _main():
+    """
+    Parse options and run checks on Python source.
+    """
+    options, args = process_options()
+    if options.doctest:
+        import doctest
+        return doctest.testmod()
+    start_time = time.time()
+    for path in args:
+        if os.path.isdir(path):
+            input_dir(path)
+        else:
+            input_file(path)
+    elapsed = time.time() - start_time
+    if options.statistics:
+        print_statistics()
+    if options.benchmark:
+        print_benchmark(elapsed)
+
+
+if __name__ == '__main__':
+    _main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_error.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - MoinMoin.error Tests
+
+    @copyright: 2003-2004 by Nir Soffer <nirs AT freeshell DOT org>,
+                2007 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import py
+
+from MoinMoin import error
+
+
+class TestEncoding(object):
+    """ MoinMoin errors do work with unicode transparently """
+
+    def testCreateWithUnicode(self):
+        """ error: create with unicode """
+        err = error.Error(u'טעות')
+        assert unicode(err) == u'טעות'
+        assert str(err) == 'טעות'
+
+    def testCreateWithEncodedString(self):
+        """ error: create with encoded string """
+        err = error.Error('טעות')
+        assert unicode(err) == u'טעות'
+        assert str(err) == 'טעות'
+
+    def testCreateWithObject(self):
+        """ error: create with any object """
+        class Foo:
+            def __unicode__(self):
+                return u'טעות'
+            def __str__(self):
+                return 'טעות'
+
+        err = error.Error(Foo())
+        assert unicode(err) == u'טעות'
+        assert str(err) == 'טעות'
+
+    def testAccessLikeDict(self):
+        """ error: access error like a dict """
+        test = 'value'
+        err = error.Error(test)
+        assert '%(message)s' % err == test
+
+coverage_modules = ['MoinMoin.error']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_sourcecode.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,108 @@
+"""
+Verify that the MoinMoin source files conform (mostly) to PEP8 coding style.
+
+Additionally, we check that the files have no crlf (Windows style) line endings.
+
+@copyright: 2006 by Armin Rigo (originally only testing for tab chars),
+            2007 adapted and extended (calling the PEP8 checker for most stuff) by MoinMoin:ThomasWaldmann.
+@license: MIT licensed
+"""
+
+import re, time
+import py
+import pep8
+
+
+moindir = py.path.local(__file__).pypkgpath()
+
+EXCLUDE = set([
+    moindir/'static', # this is our dist static stuff
+    moindir/'_tests/wiki', # this is our test wiki
+])
+
+TRAILING_SPACES = 'nochange' # 'nochange' or 'fix'
+                             # use 'fix' with extreme caution and in a separate changeset!
+FIX_TS_RE = re.compile(r' +$', re.M) # 'fix' mode: everything matching the trailing space re will be removed
+
+try:
+    import xattr
+    if not hasattr(xattr, "xattr"): # there seem to be multiple modules with that name
+        raise ImportError
+    def mark_file_ok(path, mtime):
+        x = xattr.xattr(path)
+        try:
+            x.set('user.moin-pep8-tested-mtime', '%d' % mtime)
+        except IOError:
+            # probably not supported
+            global mark_file_ok
+            mark_file_ok = lambda path, mtime: None
+
+    def should_check_file(path, mtime):
+        x = xattr.xattr(path)
+        try:
+            mt = x.get('user.moin-pep8-tested-mtime')
+            mt = long(mt)
+            return mtime > mt
+        except IOError:
+            # probably not supported
+            global should_check_file
+            should_check_file = lambda path, mtime: True
+        return True
+except ImportError:
+    def mark_file_ok(path, mtime):
+        pass
+    def should_check_file(path, mtime):
+        return True
+
+RECENTLY = time.time() - 7 * 24*60*60 # we only check stuff touched recently.
+#RECENTLY = 0 # check everything!
+
+# After doing a fresh clone, this procedure is recommended:
+# 1. Run the tests once to see if everything is OK (as cloning updates the mtime,
+#    it will test every file).
+# 2. Before starting to make new changes, use "touch" to change all timestamps
+#    to a time before <RECENTLY>.
+# 3. Regularly run the tests, they will run much faster now.
+
+def pep8_error_count(path):
+    # process_options initializes some data structures and MUST be called before each Checker().check_all()
+    pep8.process_options(['pep8', '--ignore=E202,E221,E222,E241,E301,E302,E401,E501,E701,W391,W601,W602', '--show-source', 'dummy_path'])
+    error_count = pep8.Checker(path).check_all()
+    return error_count
+
+def check_py_file(reldir, path, mtime):
+    if TRAILING_SPACES == 'fix':
+        f = file(path, 'rb')
+        data = f.read()
+        f.close()
+        fixed = FIX_TS_RE.sub('', data)
+
+        # Don't write files if there's no need for that,
+        # as altering timestamps can be annoying with some tools.
+        if fixed == data:
+            return
+
+        f = file(path, 'wb')
+        f.write(fixed)
+        f.close()
+    # Please read and follow PEP8 - rerun this test until it does not fail any more,
+    # any type of error is only reported ONCE (even if there are multiple).
+    error_count = pep8_error_count(path)
+    assert error_count == 0
+    mark_file_ok(path, mtime)
+
+def pytest_generate_tests(metafunc):
+    for pyfile in sorted(moindir.visit('*.py', lambda p: p not in EXCLUDE)):
+        relpath = moindir.bestrelpath(pyfile)
+        metafunc.addcall(id=relpath, funcargs={'path': pyfile})
+
+def test_sourcecode(path):
+    mtime = path.stat().mtime
+    if mtime < RECENTLY:
+        py.test.skip("source change not recent")
+    if not should_check_file(str(path), mtime):
+        py.test.skip("source marked as should not be checked")
+
+    check_py_file(str(moindir.bestrelpath(path)), str(path), mtime)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_test_environ.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - Tests for our test environment
+
+    @copyright: 2009 by MoinMoin:ChristopherDenter
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import py
+
+from flask import current_app as app
+from flask import flaskg
+
+from MoinMoin.items import IS_SYSITEM, SYSITEM_VERSION
+from MoinMoin.storage.error import NoSuchItemError
+
+from MoinMoin._tests import wikiconfig
+
+class TestStorageEnvironWithoutConfig(object):
+    def setup_method(self, method):
+        self.class_level_value = 123
+
+    def test_fresh_backends(self):
+        assert self.class_level_value == 123
+
+        assert isinstance(app.cfg, wikiconfig.Config)
+
+        storage = flaskg.storage
+        assert storage
+        assert hasattr(storage, 'get_item')
+        assert hasattr(storage, 'history')
+        assert not list(storage.iteritems())
+        assert not list(storage.history())
+        itemname = u"this item shouldn't exist yet"
+        assert py.test.raises(NoSuchItemError, storage.get_item, itemname)
+        item = storage.create_item(itemname)
+        new_rev = item.create_revision(0)
+        new_rev['name'] = itemname
+        new_rev['mimetype'] = u'text/plain'
+        item.commit()
+        assert storage.has_item(itemname)
+        assert not storage.has_item("FrontPage")
+
+    # Run this test twice to see if something's changed
+    test_twice = test_fresh_backends
+
+class TestStorageEnvironWithConfig(object):
+    class Config(wikiconfig.Config):
+        load_xml = wikiconfig.Config._test_items_xml
+        content_acl = dict(
+            before="+All:write", # need to write to sys pages
+            default="All:read,write,admin,create,destroy",
+            after="Me:create",
+            hierarchic=False,
+        )
+
+    def test_fresh_backends_with_content(self):
+        assert isinstance(app.cfg, wikiconfig.Config)
+
+        storage = flaskg.storage
+        should_be_there = (u"FrontPage", u"HelpOnLinking", u"HelpOnMoinWikiSyntax", )
+        for pagename in should_be_there:
+            assert storage.has_item(pagename)
+            item = storage.get_item(pagename)
+            rev = item.get_revision(-1)
+            assert rev.revno == 0
+            assert rev[IS_SYSITEM]
+            assert rev[SYSITEM_VERSION] == 1
+            # check whether this dirties the backend for the second iteration of the test
+            new_rev = item.create_revision(1)
+            new_rev['name'] = pagename
+            new_rev['mimetype'] = u'text/plain'
+            item.commit()
+
+        itemname = u"OnlyForThisTest"
+        assert not storage.has_item(itemname)
+        new_item = storage.create_item(itemname)
+        new_rev = new_item.create_revision(0)
+        new_rev['name'] = itemname
+        new_rev['mimetype'] = u'text/plain'
+        new_item.commit()
+        assert storage.has_item(itemname)
+
+        assert storage.get_backend("/").after.acl_lines[0] == "Me:create"
+
+    # Run this test twice to see if something's changed
+    test_twice = test_fresh_backends_with_content
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_user.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,391 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - MoinMoin.user Tests
+
+    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>
+                2009 by ReimarBauer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import py
+
+from flask import current_app as app
+from flask import flaskg
+
+from MoinMoin import user
+
+
+class TestEncodePassword(object):
+    """user: encode passwords tests"""
+
+    def testAscii(self):
+        """user: encode ascii password"""
+        # u'MoinMoin' and 'MoinMoin' should be encoded to same result
+        expected = "{SSHA256}n0JB8FCTQCpQeg0bmdgvTGwPKvxm8fVNjSRD+JGNs50xMjM0NQ=="
+
+        result = user.encodePassword("MoinMoin", salt='12345')
+        assert result == expected
+        result = user.encodePassword(u"MoinMoin", salt='12345')
+        assert result == expected
+
+    def testUnicode(self):
+        """ user: encode unicode password """
+        result = user.encodePassword(u'סיסמה סודית בהחלט', salt='12345') # Hebrew
+        expected = "{SSHA256}pdYvYv+4Vph259sv/HAm7zpZTv4sBKX9ITOX/m00HMsxMjM0NQ=="
+        assert result == expected
+
+
+class TestLoginWithPassword(object):
+    """user: login tests"""
+
+    def setup_method(self, method):
+        # Save original user
+        self.saved_user = flaskg.user
+
+        # Create anon user for the tests
+        flaskg.user = user.User()
+
+        self.user = None
+
+    def teardown_method(self, method):
+        """ Run after each test
+
+        Remove user and reset user listing cache.
+        """
+        # Remove user file and user
+        if self.user is not None:
+            del self.user
+
+        # Restore original user
+        flaskg.user = self.saved_user
+
+    def testAsciiPassword(self):
+        """ user: login with ascii password """
+        # Create test user
+        name = u'__Non Existent User Name__'
+        password = name
+        self.createUser(name, password)
+
+        # Try to "login"
+        theUser = user.User(name=name, password=password)
+        assert theUser.valid
+
+    def testUnicodePassword(self):
+        """ user: login with non-ascii password """
+        # Create test user
+        name = u'__שם משתמש לא קיים__' # Hebrew
+        password = name
+        self.createUser(name, password)
+
+        # Try to "login"
+        theUser = user.User(name=name, password=password)
+        assert theUser.valid
+
+    def test_auth_with_ssha_stored_password(self):
+        """
+        Create user with {SSHA} password and check that user can login.
+        """
+        # Create test user
+        name = u'Test User'
+        # pass = 12345
+        # salt = salt
+        password = '{SSHA}x4YEGdfI4i0qROaY3NTHCmwSJY5zYWx0'
+        self.createUser(name, password, True)
+
+        # Try to "login"
+        theuser = user.User(name=name, password='12345')
+        assert theuser.valid
+
+    def test_auth_with_apr1_stored_password(self):
+        """
+        Create user with {APR1} password and check that user can login.
+        """
+        # Create test user
+        name = u'Test User'
+        # generated with "htpasswd -nbm blaze 12345"
+        password = '{APR1}$apr1$NG3VoiU5$PSpHT6tV0ZMKkSZ71E3qg.' # 12345
+        self.createUser(name, password, True)
+
+        # Try to "login"
+        theuser = user.User(name=name, password='12345')
+        assert theuser.valid
+
+    def test_auth_with_md5_stored_password(self):
+        """
+        Create user with {MD5} password and check that user can login.
+        """
+        # Create test user
+        name = u'Test User'
+        password = '{MD5}$1$salt$etVYf53ma13QCiRbQOuRk/' # 12345
+        self.createUser(name, password, True)
+
+        # Try to "login"
+        theuser = user.User(name=name, password='12345')
+        assert theuser.valid
+
+    def test_auth_with_des_stored_password(self):
+        """
+        Create user with {DES} password and check that user can login.
+        """
+        # Create test user
+        name = u'Test User'
+        # generated with "htpasswd -nbd blaze 12345"
+        password = '{DES}gArsfn7O5Yqfo' # 12345
+        self.createUser(name, password, True)
+
+        try:
+            import crypt
+            # Try to "login"
+            theuser = user.User(name=name, password='12345')
+            assert theuser.valid
+        except ImportError:
+            py.test.skip("Platform does not provide crypt module!")
+
+    def test_auth_with_ssha256_stored_password(self):
+        """
+        Create user with {SSHA256} password and check that user can login.
+        """
+        # Create test user
+        name = u'Test User'
+        # generated with online sha256 tool
+        # pass: 12345
+        # salt: salt
+        # base64 encoded
+        password = '{SSHA256}r4ONZUfEyn9MUkcyDQkQ5MBNpdIerM24MasxFpuQBaFzYWx0'
+
+        self.createUser(name, password, True)
+
+        # Try to "login"
+        theuser = user.User(name=name, password='12345')
+        assert theuser.valid
+
+    def test_regression_user_password_started_with_sha(self):
+        # This is regression test for bug in function 'user.create_user'.
+        #
+        # This function does not encode passwords which start with '{SHA}'
+        # It treats them as already encoded SHA hashes.
+        #
+        # If user during registration specifies password starting with '{SHA}'
+        # this password will not get encoded and user object will get saved with empty enc_password
+        # field.
+        #
+        # Such situation leads to "KeyError: 'enc_password'" during
+        # user authentication.
+
+        # Any Password begins with the {SHA} symbols led to
+        # "KeyError: 'enc_password'" error during user authentication.
+        user_name = u'moin'
+        user_password = u'{SHA}LKM56'
+        user.create_user(user_name, user_password, u'moin@moinmo.in', '')
+
+        # Try to "login"
+        theuser = user.User(name=user_name, password=user_password)
+        assert theuser.valid
+
+    def testSubscriptionSubscribedPage(self):
+        """ user: tests isSubscribedTo  """
+        pagename = u'HelpMiscellaneous'
+        name = u'__Jürgen Herman__'
+        password = name
+        self.createUser(name, password)
+        # Login - this should replace the old password in the user file
+        theUser = user.User(name=name, password=password)
+        theUser.subscribe(pagename)
+        assert theUser.isSubscribedTo([pagename]) # list(!) of pages to check
+
+    def testSubscriptionSubPage(self):
+        """ user: tests isSubscribedTo on a subpage """
+        pagename = u'HelpMiscellaneous'
+        testPagename = u'HelpMiscellaneous/FrequentlyAskedQuestions'
+        name = u'__Jürgen Herman__'
+        password = name
+        self.createUser(name, password)
+        # Login - this should replace the old password in the user file
+        theUser = user.User(name=name, password=password)
+        theUser.subscribe(pagename)
+        assert not theUser.isSubscribedTo([testPagename]) # list(!) of pages to check
+
+    def testRenameUser(self):
+        """ create user and then rename user and check if it still
+        exists under old name
+        """
+        # Create test user
+        name = u'__Some Name__'
+        password = name
+        self.createUser(name, password)
+        # Login - this should replace the old password in the user file
+        theUser = user.User(name=name)
+        # Rename user
+        theUser.name = u'__SomeName__'
+        theUser.save()
+        theUser = user.User(name=name, password=password)
+
+        assert not theUser.exists()
+
+    def test_upgrade_password_from_ssha_to_ssha256(self):
+        """
+        Create user with {SSHA} password and check that logging in
+        upgrades to {SSHA256}.
+        """
+        name = u'/no such user/'
+        #pass = MoinMoin
+        #salr = 12345
+        password = '{SSHA}xkDIIx1I7A4gC98Vt/+UelIkTDYxMjM0NQ=='
+        self.createUser(name, password, True)
+
+        # User is not required to be valid
+        theuser = user.User(name=name, password='12345')
+        assert theuser.enc_password[:9] == '{SSHA256}'
+
+    def test_upgrade_password_from_sha_to_ssha256(self):
+        """
+        Create user with {SHA} password and check that logging in
+        upgrades to {SSHA256}.
+        """
+        name = u'/no such user/'
+        password = '{SHA}jLIjfQZ5yojbZGTqxg2pY0VROWQ=' # 12345
+        self.createUser(name, password, True)
+
+        # User is not required to be valid
+        theuser = user.User(name=name, password='12345')
+        assert theuser.enc_password[:9] == '{SSHA256}'
+
+    def test_upgrade_password_from_apr1_to_ssha256(self):
+        """
+        Create user with {APR1} password and check that logging in
+        upgrades to {SSHA256}.
+        """
+        # Create test user
+        name = u'Test User'
+        # generated with "htpasswd -nbm blaze 12345"
+        password = '{APR1}$apr1$NG3VoiU5$PSpHT6tV0ZMKkSZ71E3qg.' # 12345
+        self.createUser(name, password, True)
+
+        # User is not required to be valid
+        theuser = user.User(name=name, password='12345')
+        assert theuser.enc_password[:9] == '{SSHA256}'
+
+    def test_upgrade_password_from_md5_to_ssha256(self):
+        """
+        Create user with {MD5} password and check that logging in
+        upgrades to {SSHA}.
+        """
+        # Create test user
+        name = u'Test User'
+        password = '{MD5}$1$salt$etVYf53ma13QCiRbQOuRk/' # 12345
+        self.createUser(name, password, True)
+
+        # User is not required to be valid
+        theuser = user.User(name=name, password='12345')
+        assert theuser.enc_password[:9] == '{SSHA256}'
+
+    def test_upgrade_password_from_des_to_ssha256(self):
+        """
+        Create user with {DES} password and check that logging in
+        upgrades to {SSHA}.
+        """
+        # Create test user
+        name = u'Test User'
+        # generated with "htpasswd -nbd blaze 12345"
+        password = '{DES}gArsfn7O5Yqfo' # 12345
+        self.createUser(name, password, True)
+
+        # User is not required to be valid
+        theuser = user.User(name=name, password='12345')
+        assert theuser.enc_password[:9] == '{SSHA256}'
+
+    def test_for_email_attribute_by_name(self):
+        """
+        checks for no access to the email attribute by getting the user object from name
+        """
+        name = u"__TestUser__"
+        password = u"ekfdweurwerh"
+        email = "__TestUser__@moinhost"
+        self.createUser(name, password, email=email)
+        theuser = user.User(name=name)
+        assert theuser.email is None
+
+    def test_for_email_attribut_by_uid(self):
+        """
+        checks access to the email attribute by getting the user object from the uid
+        """
+        name = u"__TestUser2__"
+        password = u"ekERErwerwerh"
+        email = "__TestUser2__@moinhost"
+        self.createUser(name, password, email=email)
+        uid = user.getUserId(name)
+        theuser = user.User(uid)
+        assert theuser.email == email
+
+    # Helpers ---------------------------------------------------------
+
+    def createUser(self, name, password, pwencoded=False, email=None):
+        """ helper to create test user
+        """
+        # Create user
+        self.user = user.User()
+        self.user.name = name
+        self.user.email = email
+        if not pwencoded:
+            password = user.encodePassword(password)
+        self.user.enc_password = password
+
+        # Validate that we are not modifying existing user data file!
+        if self.user.exists():
+            self.user = None
+            py.test.skip("Test user exists, will not override existing user data file!")
+
+        # Save test user
+        self.user.save()
+
+        # Validate user creation
+        if not self.user.exists():
+            self.user = None
+            py.test.skip("Can't create test user")
+
+
+class TestGroupName(object):
+
+    def testGroupNames(self):
+        """ user: isValidName: reject group names """
+        test = u'AdminGroup'
+        assert not user.isValidName(test)
+
+
+class TestIsValidName(object):
+
+    def testNonAlnumCharacters(self):
+        """ user: isValidName: reject unicode non alpha numeric characters
+
+        : and , used in acl rules, we might add more characters to the syntax.
+        """
+        invalid = u'! # $ % ^ & * ( ) = + , : ; " | ~ / \\ \u0000 \u202a'.split()
+        base = u'User%sName'
+        for c in invalid:
+            name = base % c
+            assert not user.isValidName(name)
+
+    def testWhitespace(self):
+        """ user: isValidName: reject leading, trailing or multiple whitespace """
+        cases = (
+            u' User Name',
+            u'User Name ',
+            u'User   Name',
+            )
+        for test in cases:
+            assert not user.isValidName(test)
+
+    def testValid(self):
+        """ user: isValidName: accept names in any language, with spaces """
+        cases = (
+            u'Jürgen Hermann', # German
+            u'ניר סופר', # Hebrew
+            u'CamelCase', # Good old camel case
+            u'가각간갇갈 갉갊감 갬갯걀갼' # Hangul (gibberish)
+            )
+        for test in cases:
+            assert user.isValidName(test)
+
+
+coverage_modules = ['MoinMoin.user']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_wikiutil.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,179 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.wikiutil Tests
+
+    @copyright: 2003-2004 by Juergen Hermann <jh@web.de>,
+                2007 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import py
+
+from flask import current_app as app
+
+from MoinMoin import config, wikiutil
+from MoinMoin._tests import wikiconfig
+
+from werkzeug import MultiDict
+
+
+class TestCleanInput(object):
+    def testCleanInput(self):
+        tests = [(u"", u""), # empty
+                 (u"aaa\r\n\tbbb", u"aaa   bbb"), # ws chars -> blanks
+                 (u"aaa\x00\x01bbb", u"aaabbb"), # strip weird chars
+                 (u"a"*500, u""), # too long
+                ]
+        for instr, outstr in tests:
+            assert wikiutil.clean_input(instr) == outstr
+
+
+class TestSystemItem(object):
+    systemItems = (
+        'HelpOnMoinWikiSyntax',
+        'HelpOnLinking',
+        )
+    notSystemItems = (
+        'NoSuchPageYetAndWillNeverBe',
+        )
+
+    class Config(wikiconfig.Config):
+        load_xml = wikiconfig.Config._test_items_xml
+
+    def testSystemItem(self):
+        """wikiutil: good system item names accepted, bad rejected"""
+        for name in self.systemItems:
+            assert wikiutil.isSystemItem(name)
+        for name in self.notSystemItems:
+            assert not wikiutil.isSystemItem(name)
+
+
+class TestAnchorNames(object):
+    def test_anchor_name_encoding(self):
+        tests = [
+            # text                    expected output
+            (u'\xf6\xf6ll\xdf\xdf',   'A.2BAPYA9g-ll.2BAN8A3w-'),
+            (u'level 2',              'level_2'),
+            (u'level_2',              'level_2'),
+            (u'',                     'A'),
+            (u'123',                  'A123'),
+            # make sure that a valid anchor is not modified:
+            (u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:_.-',
+             u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:_.-')
+        ]
+        for text, expected in tests:
+            yield self._check, text, expected
+
+    def _check(self, text, expected):
+        encoded = wikiutil.anchor_name_from_text(text)
+        assert expected == encoded
+
+
+class TestRelativeTools(object):
+    tests = [
+        # test                      expected output
+        # CHILD_PREFIX
+        (('MainPage', '/SubPage1'), 'MainPage/SubPage1'),
+        (('MainPage', '/SubPage1/SubPage2'), 'MainPage/SubPage1/SubPage2'),
+        (('MainPage/SubPage1', '/SubPage2/SubPage3'), 'MainPage/SubPage1/SubPage2/SubPage3'),
+        (('', '/OtherMainPage'), 'OtherMainPage'), # strange
+        # PARENT_PREFIX
+        (('MainPage/SubPage', '../SisterPage'), 'MainPage/SisterPage'),
+        (('MainPage/SubPage1/SubPage2', '../SisterPage'), 'MainPage/SubPage1/SisterPage'),
+        (('MainPage/SubPage1/SubPage2', '../../SisterPage'), 'MainPage/SisterPage'),
+        (('MainPage', '../SisterPage'), 'SisterPage'), # strange
+    ]
+    def test_abs_pagename(self):
+        for (current_page, relative_page), absolute_page in self.tests:
+            yield self._check_abs_pagename, current_page, relative_page, absolute_page
+
+    def _check_abs_pagename(self, current_page, relative_page, absolute_page):
+        assert absolute_page == wikiutil.AbsItemName(current_page, relative_page)
+
+    def test_rel_pagename(self):
+        for (current_page, relative_page), absolute_page in self.tests:
+            yield self._check_rel_pagename, current_page, absolute_page, relative_page
+
+    def _check_rel_pagename(self, current_page, absolute_page, relative_page):
+        assert relative_page == wikiutil.RelItemName(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, app.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, app.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, app.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, app.cfg)
+            assert result == expected
+
+class TestGroupItems(object):
+
+    def testNormalizeGroupName(self):
+        """ request: normalize itemname: 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.isGroupItem(test):
+                result = wikiutil.normalize_pagename(test, app.cfg)
+                assert result == expected
+
+
+coverage_modules = ['MoinMoin.wikiutil']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/testitems.xml	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,119 @@
+<backend><item name="HelpOnLinking"><meta></meta>
+<revision revno="0"><meta><entry key="hostname"><str>0.0.0.0</str>
+</entry>
+<entry key="#"><tuple><str>Please edit system and help pages ONLY in the master wiki!</str>
+<str>For more information, please see MoinMoin:MoinDev/Translation.</str>
+<str>master-page:Unknown-Page</str>
+<str>master-date:Unknown-Date</str>
+</tuple>
+</entry>
+<entry key="sha1"><str>c9b17c6d4d6ea0b59a040dde48c8cb0286ba9822</str>
+</entry>
+<entry key="userid"><str></str>
+</entry>
+<entry key="name"><str>HelpOnLinking</str>
+</entry>
+<entry key="language"><str>en</str>
+</entry>
+<entry key="comment"><str></str>
+</entry>
+<entry key="address"><str>0.0.0.0</str>
+</entry>
+<entry key="acl"><str>-All:write Default</str>
+</entry>
+<entry key="syspage_version"><int>1</int>
+</entry>
+<entry key="mimetype"><str>text/x.moin.wiki</str>
+</entry>
+<entry key="action"><str>SAVE</str>
+</entry>
+<entry key="extra"><str></str>
+</entry>
+<entry key="is_syspage"><bool>True</bool>
+</entry>
+</meta>
+<data coding="base64"><chunk>PSBMaW5raW5nIFJlZmVyZW5jZSA9DQo8PFRhYmxlT2ZDb250ZW50cz4+DQpUaGVyZSBhcmUgdHdvIGZ1bmRhbWVudGFsIGxpbmtpbmcgdHlwZXMgb24gIU1vaW5Nb2luLg0KDQogKiBCcmFja2V0cyAoYFtbICBdXWApIGFyZSB1c2VkIHRvICcnbGluaycnIHRvIGEgdGFyZ2V0IChhIGxvY2FsIHdpa2kgcGFnZSwgYW4gZXh0ZXJuYWwgVVJMLCBhIGZpbGUsIGV0YykuDQogKiBCcmFjZXMgKGB7eyAgfX1gKSBhcmUgdXNlZCB0byAnJ2VtYmVkJycgKHRyYW5zY2x1ZGUvaW5jbHVkZS9zaG93KSBzb21lIHRhcmdldCAob2Z0ZW4gYW4gaW1hZ2UsIGJ1dCBjYW4gYmUgdGV4dCkuDQoNClRhcmdldHMgYXJlIHZlcnkgZmxleGlibGUgaW4gdGhhdCB0aGV5IHN1cHBvcnQgdmlydHVhbGx5IGFueSBzdHlsZSB5b3UgY2FuIHdpdGggcmVndWxhciBIVE1MIChyZWxhdGl2ZSBvciBhYnNvbHV0ZSBwYXRocywgYW5jaG9ycywgZXRjKS4NCg0KPT0gTGlua2luZyA9PQ0KUHJvcGVybHktZm9ybWVkIFVSTHMgKGkuZS4ge3t7aHR0cDovL2V4YW1wbGUubmV0fX19KSwgSW50ZXJXaWtpIHBhZ2VzLCBlbWFpbCBhZGRyZXNzZXMsIGFuZCBDYW1lbENhc2UgcGFnZW5hbWVzIGFyZSBhdXRvbWF0aWNhbGx5IHJlY29nbml6ZWQgYXMgdGFyZ2V0cyBldmVuIHdpdGhvdXQgdXNpbmcgYnJhY2tldHMuDQp8fDx0YWJsZXdpZHRoPSIxMDAlInJvd2JnY29sb3I9IiNmZmZmY2MiMjUlPicnJ0Rlc2NyaXB0aW9uJycnIHx8JycnU3ludGF4JycnIHx8JycnQ29tbWVudCcnJyB8fA0KfHxpbnRlcm5hbCBsaW5rIHx8YFdpa2lOYW1lYCB8fENhbWVsQ2FzZSBwYWdlIG5hbWUgfHwNCnx8aW50ZXJuYWwgZnJlZSBsaW5rIHx8YFtbZnJlZSBsaW5rXV1gIHx8YW55IHBhZ2UgbmFtZSB8fA0KfHxpbnRlcm5hbCBsaW5rIHRvIHN1YiBwYWdlIHx8YC9TdWJQYWdlYCBvciBgW1svc3ViIHBhZ2VdXWAgfHwgfHwNCnx8aW50ZXJuYWwgbGluayB0byBzaXN0ZXIgcGFnZSB8fGAuLi9TaXN0ZXJQYWdlYCBvciBgW1suLi9TaXN0ZXJQYWdlfGxpbmsgdGV4dF1dYCB8fCB8fA0KfHxpbnRlcm5hbCBsaW5rIHdpdGggbGlua3RleHQgfHxgW1tTb21lUGFnZXxzb21lIFBhZ2VdXWAgfHwgfHwNCnx8aW50ZXJuYWwgbGluayB0byBhIHNlY3Rpb24gfHxgW1tTb21lUGFnZSNzdWJzZWN0aW9ufHN1YnNlY3Rpb24gb2YgU29tZSBQYWdlXV1gIHx8IFNlZSBbWyNBbmNob3JzXV0gc2VjdGlvbiBiZWxvdyB8fA0KfHxpbnRlcm5hbCBsaW5rIHdpdGggbGlua3RleHQgJiBwYXJhbWV0ZXIgfHxgW1tTb21lUGFnZXxzb21lIFBhZ2V8dGFyZ2V0PSJfYmxhbmsiXV1gIHx8c2VlIGJlbG93IGZvciBwYXJhbWV0ZXIgY29uZmlndXJhdGlvbnMgfHwNCnx8aW50ZXJuYWwgbGluayB1c2luZyBhIGdyYXBoaWMgYXMgYnV0dG9uIHx8YFtbU29tZVBhZ2V8e3thdHRhY2htZW50OmltYWdlZmlsZS5wbmd9fV1dYCB8fCB8fA0KfHxpbnRlcm5hbCBsaW5rIHVzaW5nIGdyYXBoaWMgYnV0dG9uLCBvcGVuIG5ldyB3aW5kb3cgfHxgW1tTb21lUGFnZXx7e2F0dGFjaG1lbnQ6c2FtcGxlZ3JhcGhpYy5wbmd9fXx0YXJnZXQ9Il9ibGFuayJdXWAgfHwgfHwNCnx8bGluayB0byBhdHRhY2htZW50IHx8YFtbYXR0YWNobWVudDppbWFnZS5wbmddXWAgfHxsaW5rcyB0byBhdHRhY2htZW50IGltYWdlIHx8DQp8fGxpbmsgdG8gYXR0YWNobWVudCBvZiBhbm90aGVyIHBhZ2UgfHxgW1thdHRhY2htZW50OlNvbWVQYWdlL2ltYWdlLnBuZ11dYCB8fGxpbmtzIHRvIGltYWdlIGF0dGFjaGVkIHRvIGEgZGlmZmVyZW50IHBhZ2UgfHwNCnx8aW50ZXJ3aWtpIGxpbmsgfHxgT3RoZXJ3aWtpOnNvbWVwYWdlYCB8fHJlcXVpcmVzIHVwcGVyY2FzZSB3aWtpbmFtZSB8fA0KfHxpbnRlcndpa2kgZnJlZSBsaW5rIHx8YFtbb3RoZXJ3aWtpOnNvbWVwYWdlXV1gIHx8YW55IHdpa2luYW1lIGluIHRoZSBtYXAgd29ya3MgfHwNCnx8ZXh0ZXJuYWwgbGluayB8fGBodHRwOi8vZXhhbXBsZS5uZXQvYCB8fCB8fA0KfHxleHRlcm5hbCBsaW5rIHdpdGggbGlua3RleHQgfHxgW1todHRwOi8vZXhhbXBsZS5uZXQvfGV4YW1wbGUgc2l0ZV1dYCB8fCB8fA0KfHxleHRlcm5hbCBsaW5rIHdpdGggbGlua3RleHQsIG9wZW4gbmV3IHdpbmRvdyB8fGBbW2h0dHA6Ly9leGFtcGxlLm5ldC98ZXhhbXBsZSBzaXRlfHRhcmdldD0iX2JsYW5rIl1dYCB8fHNlZSBiZWxvdyBmb3IgcGFyYW1ldGVyIGNvbmZpZ3VyYXRpb25zIHx8DQp8fGV4dGVybmFsIGxpbmsgdXNpbmcgYSBncmFwaGljIGFzIGJ1dHRvbiB8fGBbW2h0dHA6Ly9leGFtcGxlLm5ldC98e3thdHRhY2htZW50OnNhbXBsZWdyYXBoaWMucG5nfX1dXWAgfHwgfHwNCnx8ZXh0ZXJuYWwgbGluayB1c2luZyBncmFwaGljIGJ1dHRvbiwgb3BlbiBuZXcgd2luZG93IHx8YFtbaHR0cDovL2V4YW1wbGUubmV0L3x7e2F0dGFjaG1lbnQ6c2FtcGxlZ3JhcGhpYy5wbmd9fXx0YXJnZXQ9Il9ibGFuayJdXWAgfHwgfHwNCnx8Ym9yZGVyIG9mIGludGVybmFsIGxpbmsgfHx7e3tXaWtpTmFtZWBgc319fSB8fDIgYmFja3RpY2tzIC0gZm9yIHdoZW4gYSAhV2lraU5hbWUgZW5kcyBpbiB0aGUgbWlkZGxlIG9mIGEgd29yZCB8fA0KfHxhdm9pZCBhbiBpbnRlcm5hbCBsaW5rIHx8YCFXaWtpTmFtZWAgfHxjb25maWd1cmFibGUgZnVuY3Rpb24gfHwNCg0KPDxCUj4+DQoNCj09IEVtYmVkZGluZyA9PQ0KRW1iZWRkaW5nL1RyYW5zY2x1c2lvbiBpcyB1c2VkIGlmIHlvdSB3YW50IHRvIGluY2x1ZGUgYW4gZXh0ZXJuYWwgZmlsZSB3aXRoaW4geW91ciB3aWtpLiAgTW9zdCBjb21tb25seSwgdGhpcyB3aWxsIGJlIGEgZ3JhcGhpYy4gQnV0IGl0IGNhbiBhbHNvIGJlIGEgdGV4dCBmaWxlIG9yIGFueSBvdGhlciBmaWxlIHRoYXQgdGhlIHdpa2kgdW5kZXJzdGFuZHMgKGZvciBleGFtcGxlLCB5b3UgY2FuIHByb3ZpZGUgbGluayBmb3IgZG93bmxvYWRpbmcgJydhbmQnJyBkaXNwbGF5IHRoZSBjb250ZW50cyBvZiB0aGF0IGZpbGUgb24gdGhlIHBhZ2UhKS4NCg0KJydzZWUgSGVscE9uSW1hZ2VzIGZvciBleGFtcGxlcy4nJw0KDQp8fDx0YWJsZXdpZHRoPSIxMDAlInJvd2JnY29sb3I9IiNmZmZmY2MiMjUlPicnJ0Rlc2NyaXB0aW9uJycnIHx8JycnU3ludGF4JycnIHx8JycnQ29tbWVudCcnJyB8fA0KfHxlbWJlZCBhbiBhdHRhY2hlZCBncmFwaGljcyB8fGB7e2F0dGFjaG1lbnQ6aW1hZ2UucG5nfX1gIHx8c2hvdyBhdHRhY2hlZCBpbWFnZSBmaWxlIGBpbWFnZS5wbmdgIHx8DQp8fGVtYmVkIGFuIGF0dGFjaGVkIGdyYXBoaWNzIGFuZCBnaXZlIGFsdCB0ZXh0IHx8YHt7YXR0YWNobWVudDppbWFnZS5wbmd8YWx0IHRleHR9fWAgfHxzaG93IGF0dGFjaGVkIGltYWdlIGZpbGUgYGltYWdlLnBuZ2Agd2l0aCBhbHRlcm5hdGl2ZSB0ZXh0IHNheWluZyBgYWx0IHRleHRgIChyZWNvbW1lbmRlZCBmb3IgYWNjZXNzaWJpbGl0eSkgfHwNCnx8ZW1iZWQgYW4gYXR0YWNoZWQgZ3JhcGhpY3MgYW5kIGRlZmluZSBhbGlnbm1lbnQgfHxge3thdHRhY2htZW50OmltYWdlLnBuZ3xhbHQgdGV4dHxhbGlnbj0icG9zaXRpb24ifX1gIHx8c2hvdyBhdHRhY2hlZCBpbWFnZSBmaWxlIGBpbWFnZS5wbmdgIHdpdGggYWx0ZXJuYXRpdmUgdGV4dCBgYWx0IHRleHRgIGFuZCAgYWxpZ25lZCB0byAnJ3Bvc2l0aW9uJycsIHdoZXJlICcncG9zaXRpb24nJyBjYW4gYmUgb25lIG9mICcnJ3RvcCcnJywgJycnbWlkZGxlJycnLCAnJydib3R0b20nJycsICcnJ3JpZ2h0JycnIG9yICcnJ2xlZnQnJycgKGRvIG5vdCBvbWl0IGFsdCB0ZXh0KSB8fA0KfHxlbWJlZCBhbiBhdHRhY2hlZCBncmFwaGljcyBhbmQgcmVzaXplIGl0IHx8YHt7YXR0YWNobWVudDppbWFnZS5wbmd8YWx0IHRleHR8d2lkdGg9MTAwIGhlaWdodD0xNTB9fWAgfHxzaG93IGF0dGFjaGVkIGltYWdlIGZpbGUgYGltYWdlLnBuZ2Agd2l0aCBhbHRlcm5hdGl2ZSB0ZXh0IGBhbHQgdGV4dGAgYW5kIHJlc2l6ZSBpdCB0byAxMDBweCB3aWR0aCBhbmQgMTUwcHggaGlnaCAoZG8gbm90IG9taXQgYWx0IHRleHQpIHx8DQp8fGVtYmVkIGFuIGV4dGVybmFsIGdyYXBoaWNzIHx8YHt7aHR0cDovL2V4YW1wbGUubmV0L2ltYWdlLnBuZ319YCB8fHNob3cgdGFyZ2V0IGltYWdlIGlubGluZSB8fA0KfHxlbQ==</chunk>
+<chunk>YmVkIGFuIGV4dGVybmFsIGdyYXBoaWNzIGFuZCBnaXZlIGFsdCB0ZXh0IHx8YHt7aHR0cDovL2V4YW1wbGUubmV0L2ltYWdlLnBuZ3xhbHQgdGV4dH19YCB8fHNob3cgdGFyZ2V0IGltYWdlIGlubGluZSB3aXRoIGFsdGVybmF0aXZlIHRleHQgc2F5aW5nIGBhbHQgdGV4dGAgKHJlY29tbWVuZGVkIGZvciBhY2Nlc3NpYmlsaXR5KSB8fA0KfHxlbWJlZCBhbiBleHRlcm5hbCBncmFwaGljcyBhbmQgZGVmaW5lIGFsaWdubWVudCB8fGB7e2h0dHA6Ly9leGFtcGxlLm5ldC9pbWFnZS5wbmd8YWx0IHRleHR8YWxpZ249InBvc2l0aW9uIn19YCB8fHNob3cgdGFyZ2V0IGltYWdlIGlubGluZSB3aXRoIGFsdGVybmF0aXZlIHRleHQgYGFsdCB0ZXh0YCBhbmQgYWxpZ25lZCB0byAnJ3Bvc2l0aW9uJycsIHdoZXJlICcncG9zaXRpb24nJyBjYW4gYmUgb25lIG9mICcnJ3RvcCcnJywgJycnbWlkZGxlJycnLCAnJydib3R0b20nJycsICcnJ3JpZ2h0JycnLCBvciAnJydsZWZ0JycnIChkbyBub3Qgb21pdCBhbHQgdGV4dCkgfHwNCnx8ZW1iZWQgYW4gZXh0ZXJuYWwgZ3JhcGhpY3MgYW5kIHJlc2l6ZSBpdCB8fGB7e2h0dHA6Ly9leGFtcGxlLm5ldC9pbWFnZS5wbmd8YWx0IHRleHR8d2lkdGg9MTAwfX1gIHx8c2hvdyB0YXJnZXQgaW1hZ2UgaW5saW5lIHdpdGggYWx0ZXJuYXRpdmUgdGV4dCBgYWx0IHRleHRgIGFuZCByZXNpemUgaXQgdG8gMTAwcHggd2lkdGggKGRvIG5vdCBvbWl0IGFsdCB0ZXh0KSB8fA0KDQoNCj09IEV4cGxhbmF0aW9ucyA9PQ0KPT09IFVSTHMgPT09DQpJZiB5b3UgZW50ZXIgVVJMcyBpbnRvIG5vcm1hbCB0ZXh0LCB0aGVyZSBpcyB0aGUgcHJvYmxlbSBvZiBkZXRlY3Rpbmcgd2hhdCBiZWxvbmdzIHRvIHRoZSBVUkwgYW5kIHdoYXQgbm90LiBUaGVyZSBhcmUgZm91ciB3YXlzIHRvIGZvcmNlIHRoZSBlbmRpbmcgb2YgYW4gVVJMOg0KDQogKiBwdXQgYSBzcGFjZSBhZnRlciB0aGUgVVJMLA0KICogdXNlIHRoZSBXaWtpOlNpeFNpbmdsZVF1b3RlcyBlc2NhcGluZywNCiAqIHVzZSB0aGUgZG91YmxlIGJyYWNrZXRlZCBVUkwgc3ludGF4Lg0KDQpUaGUgc3VwcG9ydGVkIFVSTCBzY2hlbWVzIGFyZTogYGh0dHBgLCBgaHR0cHNgLCBgZnRwYCwgYGZpbGVgIGFuZCBzb21lIG90aGVycy4gVGhlIGFkbWluaXN0cmF0b3Igb2YgeW91ciB3aWtpIGNhbiBleHRlbmQgdGhlIHN1cHBvcnRlZCBzY2hlbWVzIGJ5IHVzaW5nIHRoZSB7e3t1cmxfc2NoZW1hc319fSB2YXJpYWJsZSAoc2VlIEhlbHBPbkNvbmZpZ3VyYXRpb24pLg0KDQpJbiBhZGRpdGlvbiB0byB0aGUgc3RhbmRhcmQgc2NoZW1lcywgdGhlcmUgYXJlIE1vaW5Nb2luLXNwZWNpZmljIG9uZXM6IGBhdHRhY2htZW50YCBhbmQgYGRyYXdpbmdgLCB0aGVzZSBhcmUgcmVsYXRlZCB0byBmaWxlIGF0dGFjaG1lbnRzIGFuZCBhcmUgZXhwbGFpbmVkIG9uIEhlbHBPbkFjdGlvbnMvQXR0YWNoRmlsZS4NCg0KPT09IFNwYWNlcyA9PT0NCllvdSBjYW4gdXNlIGRvdWJsZS1icmFja2V0cyAob3IgZG91YmxlLWJyYWNlcykgc3ludGF4IHRvIGxpbmsgdG8gYSBwYWdlIG9yIGZpbGUgbmFtZSB3aXRoIHNwYWNlcy4gVGhpcyB3aWxsIGV2ZW4gd29yayBmb3IgaW50ZXJ3aWtpIGxpbmtzLCBwcm92aWRlZCB0aGUgdGFyZ2V0IHdpa2kgdW5kZXJzdGFuZHMgc3RhbmRhcmQgdXJsIHF1b3RpbmcgKHNwYWNlcyB3aWxsIGJlY29tZSB7e3slMjB9fX0pLg0KDQpIb3dldmVyLCBiZXN0IHByYWN0aWNlIGlzIHRvIHRyeSB0byBhdm9pZCBzcGFjZXMgaW4gVVJMcywgYXMgeW91IG1heSBmaW5kIGl0J3MgbW9yZSBkaWZmaWN1bHQgdG8gd29yayB3aXRoIHRoYXQgVVJMLiBGb3IgZXhhbXBsZSwgaWYgeW91IHRyeSBjb3B5aW5nIGFuZCBlbWFpbGluZyB0aGF0IFVSTCBsaW5rLCB0aGUgcmVjZWl2ZXIgbWF5IGhhdmUgZGlmZmljdWx0eSBsYW5kaW5nIG9uIHRoZSBwYWdlIHlvdSBzcGVjaWZpZWQuDQoNCjw8QW5jaG9yKEFuY2hvcnMpPj4NCj09PSBBbmNob3JzID09PQ0KVG8gJydpbnNlcnQnJyBhbmNob3JzIGludG8gYSBwYWdlIHlvdSBuZWVkIHRoZSAnJ0FuY2hvcicnIG1hY3JvIChzZWUgSGVscE9uTWFjcm9zKTogYDw8QW5jaG9yKGFuY2hvcm5hbWUpPj5gLCB3aGVyZSAiYW5jaG9ybmFtZSIgaXMgdGhlIGFjdHVhbCBpZGVudGlmaWVyIG9mIHRoZSBhbmNob3IuDQoNClRvIGxpbmsgdG8gYW4gYW5jaG9yIG9uIHRoZSBzYW1lIHdpa2kgcGFnZSB1c2UgYFtbI2FuY2hvcm5hbWVdXWAgb3IgYFtbI2FuY2hvcm5hbWV8bGFiZWwgdGV4dF1dYC4NCg0KVG8gbGluayB0byBhbiBhbmNob3Igb24gYW5vdGhlciB3aWtpIHBhZ2Ugd3JpdGUgYFtbUGFnZU5hbWUjYW5jaG9ybmFtZV1dYCBvciBgW1tQYWdlTmFtZSNhbmNob3JuYW1lfGxhYmVsIHRleHRdXWAsIHdoZXJlICJQYWdlTmFtZSIgaXMgdGhlIG5hbWUgb2YgdGhlIG90aGVyIHBhZ2UgYW5kICJhbmNob3JuYW1lIiBpcyB0aGUgaWRlbnRpZmllciBvZiB0aGUgYW5jaG9yIG9uIHRoYXQgcGFnZS4NCg0KDQoNCj09PSBQcmV2ZW50aW5nIEF1dG9tYXRpY2FsbHkgR2VuZXJhdGVkIExpbmtzID09PQ0KDQpUbyBrZWVwIGEgd29yZCBsaWtlIFBhZ2VOYW1lIGZyb20gYXV0b21hdGljYWxseSBiZWluZyB0dXJuZWQgaW50byBhIGxpbmssIA0KeW91IGNhbiBzdXBwcmVzcyBDYW1lbENhc2UgbGlua2luZyBieSBwdXR0aW5nIGFuIGV4Y2xhbWF0aW9uIG1hcmsgKHt7eyF9fX0pIGJlZm9yZSB0aGUgd29yZCwgaS5lLiBgIVBhZ2VOYW1lYC4gVGhpcyBtZXRob2Qgd2lsbCBub3QgaW50ZXJmZXJlIHdpdGggbW9zdCBzZWFyY2hlcyAodGhlIGV4Y2VwdGlvbiBpcyBjZXJ0YWluIHF1b3RlZCBwaHJhc2VzIGFuZCByZWd1bGFyIGV4cHJlc3Npb25zKS4gDQoNCkFsdGVybmF0aXZlbHksIHlvdSBjYW4gaW5zZXJ0IHR3byBiYWNrLXRpY2tzIHt7e1BhZ2VgYE5hbWV9fX0uICBIb3dldmVyLCB0aGUgcHJvYmxlbSB3aXRoIGRvaW5nIHRoaXMgaXMgdGhhdCBpdCB3aWxsIHByZXZlbnQgYSBzaW1wbGUgc2VhcmNoIGZvciB0aGUgd29yZCAiIVBhZ2VOYW1lIiBmcm9tIG1hdGNoaW5nIHt7e1BhZ2VgYE5hbWV9fX0gaW4gYSBwYWdlLCBkdWUgdG8gdGhlIGluc2VydGVkIGNoYXJhY3RlcnMuDQoNClRvIHByZXZlbnQgYXV0b21hdGljIFVSTCBsaW5raW5nLCB1c2UgZWl0aGVyIHt7e2BodHRwOi8vLi4uYH19fSBvciBge3t7aHR0cDovLy4uLn19fWAuDQoNCg0KDQo9PT0gVXNpbmcgbGluayBwYXJhbWV0ZXJzID09PQ0KU29tZXRpbWVzIHlvdSBtYXkgd2FudCB0byBnaXZlIGFkZGl0aW9uYWwgcGFyYW1ldGVycyBmb3IgYSBsaW5rLCBpbmZsdWVuY2luZyBob3cgaXQgbG9va3MgbGlrZSwgaG93IGl0IGJlaGF2ZXMgYW5kIGhvdyBleGFjdGx5IGl0IGxpbmtzIHRvIHRoZSB0YXJnZXQgLSB0aGlzIGlzIHdoYXQgdGhlICcnJ3BhcmFtcycnJyBwYXJ0IG9mIGBbW3RhcmdldHx0ZXh0fHBhcmFtc11dYCBpcyBmb3IuDQoNCmUuZy4gaWYgeW91IHdhbnQgYSBkaXJlY3QgZG93bmxvYWQgbGluayB5b3Ugd2FudCB0byBlbnRlciBhcyBgcGFyYW0gJmRvPWdldGAgYFtbYXR0YWNobWVudDpIZWxwT25JbWFnZXMvcGluZWFwcGxlLmpwZ3xhIHBpbmVhcHBsZXwmZG89Z2V0XV1gIFtbYXR0YWNobWVudDpIZWxwT25JbWFnZXMvcGluZWFwcGxlLmpwZ3xhIHBpbmVhcHBsZXwmZG89Z2V0XV0NCg0KPT09PSBTZXR0aW5nIGF0dHJpYnV0ZXMgb2YgdGhlIDxhPiB0YWcgPT09PQ0KQXZhaWxhYmxlIGF0dHJpYnV0ZXM6IGNsYXNzLCB0aXRsZSwgdGFyZ2V0LCBhY2Nlc3NrZXkgKHNlZSBzb21lIGh0bWwgcmVmZXJlbmNlIGlmIHlvdSB3YW50IHRvIGtub3cgd2hhdCB0aGV5IG1lYW4pLg0KDQpFeGFtcGxlOiBgW1todHRwOi8vbW9pbm1vLmluL3xNb2luTW9pbiBXaWtpfGNsYXNzPWdyZWVuIGRvdHRlZCxhY2Nlc3NrZXk9MV1dYA0KDQpSZW5kZXJzIGFzOiBbW2h0dHA6Ly9tb2lubW8uaW4vfE1vaW5Nb2luIFdpa2l8Y2xhc3M9Z3JlZW4gZG90dGVkLGFjY2Vzc2tleT0xXV0NCg0KKCEpIFByZXNzaW5nIHRoZSBhY2Nlc3Mga2V5IHNob3VsZCBqdW1wIHRvIHRoYXQgbGluayB0YXJnZXQgKGZvciBGaXJlZm94IDIueCBhbmQgdGhlIGV4YW1wbGUgYWJvdmUgaXQgaXMgQWx0LVNoaWZ0LTEpLg==</chunk>
+<chunk>DQoNCj09PT0gQ3JlYXRpbmcgYSBxdWVyeSBzdHJpbmcgZm9yIHRoZSB0YXJnZXQgVVJMID09PT0NCldoYXQgaXMgcG9zc2libGUgZm9yIHRoaXMgZGVwZW5kcyBvbiB0aGUgdGFyZ2V0IHNpdGUuDQoNCkV4YW1wbGU6IGBbW01vaW5Nb2luOk1vaW5Nb2luV2lraXxNb2luTW9pbiBXaWtpfCZhY3Rpb249ZGlmZiwmcmV2MT0xLCZyZXYyPTJdXWANCg0KUmVuZGVycyBhczogW1tNb2luTW9pbjpNb2luTW9pbldpa2k/cmV2MT0xJmFjdGlvbj1kaWZmJnJldjI9MnxNb2luTW9pbiBXaWtpXV0NCg0KKCEpIFBsZWFzZSByZW1lbWJlcjoNCg0KICogSWYgeW91IHdhbnQgdG8gZ2l2ZSBhIGtleT12YWx1ZSBpdGVtIGZvciB0aGUgcXVlcnkgc3RyaW5nLCBkb24ndCBmb3JnZXQgdGhlIGFtcGVyc2FuZCAoJikuDQogKiBHaXZpbmcgcXVlcnkgc3RyaW5nIGl0ZW1zIGRvZXMgbm90IHdvcmsgd2hlbiB5b3UgZ2l2ZSBhIFVSTCBhcyB0YXJnZXQgKGJ1dCBmb3IgbGlua3MgdG8gcGFnZXMgb3IgYXR0YWNobWVudHMpLg0KICogSWYgeW91IGdpdmUgYSBVUkwgYXMgdGFyZ2V0LCB5b3UgY2FuIGluY2x1ZGUgYSBxdWVyeSBzdHJpbmcgZGlyZWN0bHkgaW4gdGhhdCB0YXJnZXQsIG5vIG5lZWQgZm9yIHBhcmFtcy4NCiAqIFlvdSBkb24ndCBuZWVkIHRvIGVuY29kZSBhbmQgdXJsX3F1b3RlIHRoZSBxdWVyeSBzdHJpbmcgc3R1ZmYsIG1vaW4gZG9lcyB0aGlzIGF1dG9tYXRpY2FsbHkgZm9yIHlvdS4NCg0KPT09PSBJbWFnZXMgPT09DQpZb3UgbWF5IHVzZQ0KDQp7e3sNCnt7YXR0YWNobWVudDppbWFnZWZpbGUucG5nfHRleHQgZGVzY3JpYmluZyBpbWFnZXx3aWR0aD0xMDB9fQ0KfX19DQp0byBoYXZlIHRoZSBhdHRhY2hlZCBmaWxlIGBpbWFnZWZpbGUucG5nYCBkaXNwbGF5ZWQgd2l0aCBhIHdpZHRoIG9mIDEwMHB4OyB0aGUgZ3JhcGhpY3MnIGhlaWdodCB3aWxsIGJlIHJlZHVjZWQvIGVubGFyZ2VkIHByb3BvcnRpb25hbGx5IChlLmcuIGlmIGBpbWFnZWZpbGUucG5nYCB3YXMgYWN0dWFsbHkgMjAwcHggd2lkdGggYW5kIDQwMHB4IGhlaWdoLCBoZWlnaHQgd291bGQgYmUgcmVkdWNlZCBpbiB0aGlzIGV4YW1wbGUgdG8gMjAwcHgpLiBZb3UgbWF5IGFsc28gdXNlDQoNCnt7ew0Ke3thdHRhY2htZW50OmltYWdlZmlsZS5wbmd8dGV4dCBkZXNjcmliaW5nIGltYWdlfGhlaWdodD0xMDB9fQ0KfX19DQp0byBoYXZlIHRoZSBhdHRhY2hlZCBmaWxlIGBpbWFnZWZpbGUucG5nYCBkaXNwbGF5ZWQgd2l0aCBhIGhlaWdodCBvZiAxMDBweCwgYW5kIHRoZSBncmFwaGljcycgd2lkdGggd2lsbCBiZSByZWR1Y2VkLyBlbmxhcmdlZCBwcm9wb3J0aW9uYWxseS4gVXNlDQoNCnt7ew0Ke3thdHRhY2htZW50OmltYWdlZmlsZS5wbmd8dGV4dCBkZXNjcmliaW5nIGltYWdlfHdpZHRoPTEwMCBoZWlnaHQ9MTUwfX0NCn19fQ0KdG8gaGF2ZSB0aGUgYXR0YWNoZWQgZmlsZSBgaW1hZ2VmaWxlLnBuZ2AgZGlzcGxheWVkIHdpdGggYSB3aWR0aCBvZiAxMDBweCBhbmQgYSBoZWlnaHQgb2YgMTUwcHguIFBsZWFzZSBkbyBub3Qgb21pdCB0aGUgYWx0ZXJuYXRpdmUgdGV4dCBpbiBuZWl0aGVyIGNhc2UuDQoNCk5vdGUgdGhpcyBkb2VzIG5vdCBhbHRlciB0aGUgYXR0YWNoZWQgZmlsZSBpdHNlbGYsIGluIG9ubHkgbWFrZXMgdGhlIGJyb3dzZXIgc2NhbGUgdGhlIGltYWdlIGRvd24vIHVwIHRvIHRoZSB2YWx1ZSBnaXZlbiB3aGlsZSBkaXNwbGF5aW5nIGl0Lg0KDQo9PT09IFRodW1ibmFpbHMgPT09PQ0KWW91IG1heSBjb21iaW5lIHRoZSB0cmFuc2NsdXNpb24gd2l0aCB0aGUgbGlua2luZyBzeW50YXgsIGxlYWRpbmcgdG8gYW4gaW1hZ2UgZGlzcGxheWVkIGluIHJlZHVjZWQgc2l6ZSB0aGF0IGxpbmtzIHRvIGl0c2VsZiBpbiBhY3R1YWwgc2l6ZSwgZS5nLg0KDQp7e3sNCltbYXR0YWNobWVudDppbWFnZWZpbGUucG5nfHt7YXR0YWNobWVudDppbWFnZWZpbGUucG5nfHRleHQgZGVzY3JpYmluZyBpbWFnZXx3aWR0aD0xMDB9fV1dDQp9fX0NCg==</chunk>
+</data>
+</revision>
+</item>
+<item name="FrontPage"><meta></meta>
+<revision revno="0"><meta><entry key="hostname"><str>0.0.0.0</str>
+</entry>
+<entry key="#"><tuple><str>Please edit system and help pages ONLY in the master wiki!</str>
+<str>For more information, please see MoinMoin:MoinDev/Translation.</str>
+<str>master-page:FrontPage</str>
+</tuple>
+</entry>
+<entry key="sha1"><str>836e7743aa94de00274aef973ac24d4e2cd4f69b</str>
+</entry>
+<entry key="userid"><str></str>
+</entry>
+<entry key="name"><str>FrontPage</str>
+</entry>
+<entry key="language"><str>en</str>
+</entry>
+<entry key="comment"><str></str>
+</entry>
+<entry key="address"><str>0.0.0.0</str>
+</entry>
+<entry key="syspage_version"><int>1</int>
+</entry>
+<entry key="mimetype"><str>text/x.moin.wiki</str>
+</entry>
+<entry key="pragma"><str>section-numbers off</str>
+</entry>
+<entry key="action"><str>SAVE</str>
+</entry>
+<entry key="extra"><str></str>
+</entry>
+<entry key="is_syspage"><bool>True</bool>
+</entry>
+</meta>
+<data coding="base64"><chunk>PSBXaWtpTmFtZSBXaWtpID0NCg0KV2hhdCBpcyB0aGlzIHdpa2kgYWJvdXQ/DQoNCkludGVyZXN0aW5nIHN0YXJ0aW5nIHBvaW50czoNCiAqIFJlY2VudENoYW5nZXM6IHNlZSB3aGVyZSBwZW9wbGUgYXJlIGN1cnJlbnRseSB3b3JraW5nDQogKiBXaWtpU2FuZEJveDogZmVlbCBmcmVlIHRvIGNoYW5nZSB0aGlzIHBhZ2UgYW5kIGV4cGVyaW1lbnQgd2l0aCBlZGl0aW5nDQogKiBGaW5kUGFnZTogZmluZCBzb21lIGNvbnRlbnQsIGV4cGxvcmUgdGhlIHdpa2kNCiAqIEhlbHBPbk1vaW5XaWtpU3ludGF4OiBxdWljayBhY2Nlc3MgdG8gd2lraSBtYXJrdXANCg0KDQo9PSBIb3cgdG8gdXNlIHRoaXMgc2l0ZSA9PQ0KDQpBIFdpa2kgaXMgYSBjb2xsYWJvcmF0aXZlIHNpdGUsIGFueW9uZSBjYW4gY29udHJpYnV0ZSBhbmQgc2hhcmU6DQogKiBFZGl0IGFueSBwYWdlIGJ5IHByZXNzaW5nICcnJzw8R2V0VGV4dChFZGl0KT4+JycnIGF0IHRoZSB0b3Agb3IgdGhlIGJvdHRvbSBvZiB0aGUgcGFnZSANCiAqIENyZWF0ZSBhIGxpbmsgdG8gYW5vdGhlciBwYWdlIHdpdGggam9pbmVkIGNhcGl0YWxpemVkIHdvcmRzIChsaWtlIFdpa2lTYW5kQm94KSBvciB3aXRoIHt7e1tbd29yZHMgaW4gYnJhY2tldHNdXX19fQ0KICogU2VhcmNoIGZvciBwYWdlIHRpdGxlcyBvciB0ZXh0IHdpdGhpbiBwYWdlcyB1c2luZyB0aGUgc2VhcmNoIGJveCBhdCB0aGUgdG9wIG9mIGFueSBwYWdlDQogKiBTZWUgSGVscEZvckJlZ2lubmVycyB0byBnZXQgeW91IGdvaW5nLCBIZWxwQ29udGVudHMgZm9yIGFsbCBoZWxwIHBhZ2VzLg0KDQpUbyBsZWFybiBtb3JlIGFib3V0IHdoYXQgYSBXaWtpV2lraVdlYiBpcywgcmVhZCBhYm91dCBNb2luTW9pbjpXaHlXaWtpV29ya3MgYW5kIHRoZSBNb2luTW9pbjpXaWtpTmF0dXJlLg0KDQpUaGlzIHdpa2kgaXMgcG93ZXJlZCBieSBbW2h0dHA6Ly9tb2lubW8uaW4vfE1vaW5Nb2luXV0uDQo=</chunk>
+</data>
+</revision>
+</item>
+<item name="HelpOnMoinWikiSyntax"><meta></meta>
+<revision revno="0"><meta><entry key="hostname"><str>0.0.0.0</str>
+</entry>
+<entry key="#"><tuple><str>Please edit system and help pages ONLY in the master wiki!</str>
+<str>For more information, please see MoinMoin:MoinDev/Translation.</str>
+<str>page was renamed from SyntaxReference</str>
+<str>master-page:Unknown-Page</str>
+<str>master-date:Unknown-Date</str>
+</tuple>
+</entry>
+<entry key="sha1"><str>3f61c5a99ce682ca6f4630a6f41af0d5c0784366</str>
+</entry>
+<entry key="userid"><str></str>
+</entry>
+<entry key="name"><str>HelpOnMoinWikiSyntax</str>
+</entry>
+<entry key="language"><str>en</str>
+</entry>
+<entry key="comment"><str></str>
+</entry>
+<entry key="address"><str>0.0.0.0</str>
+</entry>
+<entry key="acl"><str>-All:write Default</str>
+</entry>
+<entry key="syspage_version"><int>1</int>
+</entry>
+<entry key="mimetype"><str>text/x.moin.wiki</str>
+</entry>
+<entry key="action"><str>SAVE</str>
+</entry>
+<entry key="extra"><str></str>
+</entry>
+<entry key="is_syspage"><bool>True</bool>
+</entry>
+</meta>
+<data coding="base64"><chunk>PSBNb2luIFdpa2kgU3ludGF4ID0NCjw8VGFibGVPZkNvbnRlbnRzKCk+Pg0KVGhpcyBwYWdlIGFpbXMgdG8gaW50cm9kdWNlIHRoZSBtb3N0IGltcG9ydGFudCBlbGVtZW50cyBvZiBNb2luTW9pbmBgJ3Mgc3ludGF4IGF0IGEgZ2xhbmNlLCBzaG93aW5nIGZpcnN0IHRoZSBtYXJrdXAgdmVyYmF0aW0gYW5kIHRoZW4gaG93IGl0IGlzIHJlbmRlcmVkIGJ5IHRoZSB3aWtpIGVuZ2luZS4gQWRkaXRpb25hbGx5LCB5b3UnbGwgZmluZCBsaW5rcyB0byB0aGUgcmVsYXRpdmUgaGVscCBwYWdlcy4gUGxlYXNlIG5vdGUgdGhhdCBzb21lIG9mIHRoZSBmZWF0dXJlcyBkZXBlbmQgb24geW91ciBjb25maWd1cmF0aW9uLg0KDQoNCg0KPT0gSGVhZGluZ3MgYW5kIHRhYmxlIG9mIGNvbnRlbnRzID09DQonJycnJ3NlZTonJycgSGVscE9uSGVhZGxpbmVzJycNCnt7ew0KVGFibGUgb2YgY29udGVudHM6DQo8PFRhYmxlT2ZDb250ZW50cygpPj4NCg0KVGFibGUgb2YgY29udGVudHMgKHVwIHRvIDJuZCBsZXZlbCBoZWFkaW5ncyBvbmx5KToNCjw8VGFibGVPZkNvbnRlbnRzKDIpPj4NCg0KPSBoZWFkaW5nIDFzdCBsZXZlbCA9DQo9PSBoZWFkaW5nIDJuZCBsZXZlbCA9PQ0KPT09IGhlYWRpbmcgM3JkIGxldmVsID09PQ0KPT09PSBoZWFkaW5nIDR0aCBsZXZlbCA9PT09DQo9PT09PSBoZWFkaW5nIDV0aCBsZXZlbCA9PT09PQ0KPT09PT09IG5vIGhlYWRpbmcgNnRoIGxldmVsID09PT09PQ0KfX19DQp7e3sjIXdpa2kgZGFzaGVkDQpUYWJsZSBvZiBjb250ZW50czoNCjw8VGFibGVPZkNvbnRlbnRzKCk+Pg0KDQpUYWJsZSBvZiBjb250ZW50cyAodXAgdG8gMm5kIGxldmVsIGhlYWRpbmdzIG9ubHkpOg0KPDxUYWJsZU9mQ29udGVudHMoMik+Pg0KDQo9IGhlYWRpbmcgMXN0IGxldmVsID0NCj09IGhlYWRpbmcgMm5kIGxldmVsID09DQo9PT0gaGVhZGluZyAzcmQgbGV2ZWwgPT09DQo9PT09IGhlYWRpbmcgNHRoIGxldmVsID09PT0NCj09PT09IGhlYWRpbmcgNXRoIGxldmVsID09PT09DQo9PT09PT0gbm8gaGVhZGluZyA2dGggbGV2ZWwgPT09PT09DQp9fX0NCg0KPT0gVGV4dCBGb3JtYXR0aW5nID09DQonJycnJ3NlZTonJycgSGVscE9uRm9ybWF0dGluZycnDQp8fDxyb3diZ2NvbG9yPSIjZmZmZmNjIiB3aWR0aD0iNTAlIj4gJycnTWFya3VwJycnIHx8ICcnJ1Jlc3VsdCcnJyAgIHx8DQp8fCAgYCcnaXRhbGljJydgICAgICB8fCAnJ2l0YWxpYycnICAgICAgIHx8DQp8fCAgYCcnJ2JvbGQnJydgICAgICB8fCAnJydib2xkJycnICAgICAgIHx8DQp8fCAge3t7YG1vbm9zcGFjZWB9fX0gfHwgYG1vbm9zcGFjZWAgIHx8DQp8fCAgYHt7e2NvZGV9fX1gICAgICB8fCB7e3tjb2RlfX19ICAgICAgIHx8DQp8fCAgYF9fdW5kZXJsaW5lX19gICB8fCBfX3VuZGVybGluZV9fICAgfHwNCnx8ICBgXnN1cGVyXnNjcmlwdGAgIHx8IF5zdXBlcl5zY3JpcHQgICAgfHwNCnx8ICBgLCxzdWIsLHNjcmlwdGAgIHx8ICwsc3ViLCxzY3JpcHQgICAgfHwNCnx8ICBgfi1zbWFsbGVyLX5gICAgIHx8IH4tc21hbGxlci1+ICAgICB8fA0KfHwgIGB+K2xhcmdlcit+YCAgICAgfHwgfitsYXJnZXIrfiAgICAgICB8fA0KfHwgYC0tKHN0cm9rZSktLWAgICAgfHwgLS0oc3Ryb2tlKS0tICAgICB8fA0KDQoNCj09IEh5cGVybGlua3MgPT0NCicnJycnc2VlOicnJyBIZWxwT25MaW5raW5nJycNCg0KDQo9PT0gSW50ZXJuYWwgTGlua3MgPT09DQp8fDxyb3diZ2NvbG9yPSIjZmZmZmNjIiB3aWR0aD0iNTAlIj4gJycnTWFya3VwJycnIHx8ICcnJ1Jlc3VsdCcnJyB8fA0KfHwgYEZyb250UGFnZWAgfHwgRnJvbnRQYWdlIHx8DQp8fCBgW1tGcm9udFBhZ2VdXWAgfHwgW1tGcm9udFBhZ2VdXSB8fA0KfHwgYEhlbHBPbkVkaXRpbmcvU3ViUGFnZXNgIHx8IEhlbHBPbkVkaXRpbmcvU3ViUGFnZXMgfHwNCnx8IGAvU3ViUGFnZWAgfHwgL1N1YlBhZ2UgfHwNCnx8IGAuLi9TaWJsaW5nUGFnZWAgfHwgLi4vU2libGluZ1BhZ2UgfHwNCnx8IGBbW0Zyb250UGFnZXxuYW1lZCBsaW5rXV1gIHx8IFtbRnJvbnRQYWdlfG5hbWVkIGxpbmtdXSB8fA0KfHwgYFtbI2FuY2hvcm5hbWVdXWAgfHwgW1sjYW5jaG9ybmFtZV1dIHx8DQp8fCBgW1sjYW5jaG9ybmFtZXxkZXNjcmlwdGlvbl1dYCB8fCBbWyNhbmNob3JuYW1lfGRlc2NyaXB0aW9uXV0gfHwNCnx8IGBbW1BhZ2VOYW1lI2FuY2hvcm5hbWVdXWAgfHwgW1tQYWdlTmFtZSNhbmNob3JuYW1lXV0gfHwNCnx8IGBbW1BhZ2VOYW1lI2FuY2hvcm5hbWV8ZGVzY3JpcHRpb25dXWAgfHwgW1tQYWdlTmFtZSNhbmNob3JuYW1lfGRlc2NyaXB0aW9uXV0gfHwNCnx8IGBbW2F0dGFjaG1lbnQ6ZmlsZW5hbWUudHh0XV1gIHx8IFtbYXR0YWNobWVudDpmaWxlbmFtZS50eHRdXSB8fA0KDQoNCj09PSBFeHRlcm5hbCBMaW5rcyA9PT0NCnx8PHJvd2JnY29sb3I9IiNmZmZmY2MiIHdpZHRoPSI1MCUiPiAnJydNYXJrdXAnJycgfHwgJycnUmVzdWx0JycnIHx8DQp8fCBgaHR0cDovL21vaW5tby5pbi9gIHx8IGh0dHA6Ly9tb2lubW8uaW4vIHx8DQp8fCBgW1todHRwOi8vbW9pbm1vLmluL11dYCB8fCBbW2h0dHA6Ly9tb2lubW8uaW4vXV0gfHwNCnx8IGBbW2h0dHA6Ly9tb2lubW8uaW4vfE1vaW5Nb2luIFdpa2ldXWAgfHwgW1todHRwOi8vbW9pbm1vLmluL3xNb2luTW9pbiBXaWtpXV0gfHwNCnx8IGBbW2h0dHA6Ly9zdGF0aWMubW9pbm1vLmluL2xvZ29zL21vaW5tb2luLnBuZ11dYCB8fCBbW2h0dHA6Ly9zdGF0aWMubW9pbm1vLmluL2xvZ29zL21vaW5tb2luLnBuZ11dIHx8DQp8fCBge3todHRwOi8vc3RhdGljLm1vaW5tby5pbi9sb2dvcy9tb2lubW9pbi5wbmd9fWAgfHwge3todHRwOi8vc3RhdGljLm1vaW5tby5pbi9sb2dvcy9tb2lubW9pbi5wbmd9fSB8fA0KfHwgYFtbaHR0cDovL3N0YXRpYy5tb2lubW8uaW4vbG9nb3MvbW9pbm1vaW4ucG5nfG1vaW5tb2luLnBuZ11dYCB8fCBbW2h0dHA6Ly9zdGF0aWMubW9pbm1vLmluL2xvZ29zL21vaW5tb2luLnBuZ3xtb2lubW9pbi5wbmddXSB8fA0KfHwgYE1lYXRCYWxsOkludGVyV2lraWAgfHwgTWVhdEJhbGw6SW50ZXJXaWtpIHx8DQp8fCBgW01lYXRCYWxsOkludGVyV2lraXxJbnRlcldpa2kgcGFnZSBvbiBNZWF0QmFsbF1dYCB8fCBbW01lYXRCYWxsOkludGVyV2lraXxJbnRlcldpa2kgcGFnZSBvbiBNZWF0QmFsbF1dIHx8DQp8fCBgW1tmaWxlOi8vLy8vc2VydmVyL3NoYXJlL2ZpbGVuYW1lJTIwd2l0aCUyMHNwYWNlcy50eHR8bGluayB0byBmaWxlbmFtZS50eHRdXWAgfHwgW1tmaWxlOi8vLy8vc2VydmVyL3NoYXJlL2ZpbGVuYW1lJTIwd2l0aCUyMHNwYWNlcy50eHR8bGluayB0byBmaWxlbmFtZS50eHRdXSB8fA0KfHwgYHVzZXJAZXhhbXBsZS5jb21gIHx8IHVzZXJAZXhhbXBsZS5jb20gfHwNCg0KDQoNCj09PSBBdm9pZCBvciBMaW1pdCBBdXRvbWF0aWMgTGlua2luZyA9PT0NCnx8PHJvd2JnY29sb3I9IiNmZmZmY2MiIHdpZHRoPSI1MCUiPiAnJydNYXJrdXAnJycgfHwgJycnUmVzdWx0JycnIHx8DQp8fCBgV2lraScnJycnJ05hbWVgIHx8IFdpa2knJycnJydOYW1lIHx8DQp8fCB7e3tXaWtpYGBOYW1lfX19IHx8IFdpa2lgYE5hbWUgfHwNCnx8IGAhV2lraU5hbWVgIHx8ICFXaWtpTmFtZSB8fA0KfHwgYFdpa2lOYW1lJycnJycnc2AgfHwgV2lraU5hbWUnJycnJydzIHx8DQp8fCB7e3tXaWtpTmFtZWBgc319fSB8fCBXaWtpTmFtZWBgcyB8fA0KfHwge3t7YGh0dHA6Ly93d3cuZXhhbXBsZS5jb21gfX19IHx8IGBodHRwOi8vd3d3LmV4YW1wbGUuY29tYCB8fA0KfHwgYFtbaHR0cDovL3d3dy5leGFtcGxlLmNvbS9dXW5vdGxpbmtlZGAgfHwgW1todHRwOi8vd3d3LmV4YW1wbGUuY29tL11dbm90bGlua2VkIHx8DQoNCg0KPT0gRHJhd2luZ3MgPT0NCicnJycnc2VlOicnJyBIZWxwT25EcmF3aW5ncycnDQo9PT0gVFdpa2lEcmF3ID09PQ0KIHt7ZHJhd2luZzpteWV4YW1wbGV9fQ0KDQo9PT0gQW55V2lraURyYXcgPT09DQoge3tkcmF3aW5nOm15ZXhhbXBsZS5hZHJhd319DQoNCj09IEJsb2NrcXVvdGVzIGFuZCBJbmRlbnRhdGlvbnMgPT0NCnt7ew0KIGluZGVudGVkIHRleHQNCiAgdGV4dCBpbmRlbnRlZCB0byB0aGUgMm5kIGxldmVsDQp9fX0NCnt7eyMhd2lraSBkYXNoZWQNCiBpbmRlbg==</chunk>
+<chunk>dGVkIHRleHQNCiAgdGV4dCBpbmRlbnRlZCB0byB0aGUgMm5kIGxldmVsDQp9fX0NCg0KPT0gTGlzdHMgPT0NCicnJycnc2VlOicnJyBIZWxwT25MaXN0cycnDQo9PT0gVW5vcmRlcmVkIExpc3RzID09PQ0Ke3t7DQogKiBpdGVtIDENCg0KICogaXRlbSAyIChwcmVjZWRpbmcgd2hpdGUgc3BhY2UpDQogICogaXRlbSAyLjENCiAgICogaXRlbSAyLjEuMQ0KICogaXRlbSAzDQogIC4gaXRlbSAzLjEgKGJ1bGxldGxlc3MpDQogLiBpdGVtIDQgKGJ1bGxldGxlc3MpDQogICogaXRlbSA0LjENCiAgIC4gaXRlbSA0LjEuMSAoYnVsbGV0bGVzcykNCn19fQ0Ke3t7IyF3aWtpIGRhc2hlZA0KICogaXRlbSAxDQoNCiAqIGl0ZW0gMiAocHJlY2VkaW5nIHdoaXRlIHNwYWNlKQ0KICAqIGl0ZW0gMi4xDQogICAqIGl0ZW0gMi4xLjENCiAqIGl0ZW0gMw0KICAuIGl0ZW0gMy4xIChidWxsZXRsZXNzKQ0KIC4gaXRlbSA0IChidWxsZXRsZXNzKQ0KICAqIGl0ZW0gNC4xDQogICAuIGl0ZW0gNC4xLjEgKGJ1bGxldGxlc3MpDQp9fX0NCg0KPT09IE9yZGVyZWQgTGlzdHMgPT09DQo9PT09IHdpdGggTnVtYmVycyA9PT09DQp7e3sNCiAxLiBpdGVtIDENCiAgIDEuIGl0ZW0gMS4xDQogICAxLiBpdGVtIDEuMg0KIDEuIGl0ZW0gMg0KfX19DQp7e3sjIXdpa2kgZGFzaGVkDQogMS4gaXRlbSAxDQogICAxLiBpdGVtIDEuMQ0KICAgMS4gaXRlbSAxLjINCiAxLiBpdGVtIDINCn19fQ0KPT09PSB3aXRoIFJvbWFuIE51bWJlcnMgPT09PQ0Ke3t7DQogSS4gaXRlbSAxDQogICBpLiBpdGVtIDEuMQ0KICAgaS4gaXRlbSAxLjINCiBJLiBpdGVtIDINCn19fQ0Ke3t7IyF3aWtpIGRhc2hlZA0KIEkuIGl0ZW0gMQ0KICAgaS4gaXRlbSAxLjENCiAgIGkuIGl0ZW0gMS4yDQogSS4gaXRlbSAyDQp9fX0NCg0KPT09PSB3aXRoIExldHRlcnMgPT09PQ0Ke3t7DQogQS4gaXRlbSBBDQogICBhLiBpdGVtIEEuIGEpDQogICBhLiBpdGVtIEEuIGIpDQogQS4gaXRlbSBCDQp9fX0NCnt7eyMhd2lraSBkYXNoZWQNCiBBLiBpdGVtIEENCiAgIGEuIGl0ZW0gQS4gYSkNCiAgIGEuIGl0ZW0gQS4gYikNCiBBLiBpdGVtIEINCn19fQ0KPT09IERlZmluaXRpb24gTGlzdHMgPT09DQp7e3sNCiB0ZXJtOjogZGVmaW5pdGlvbg0KIG9iamVjdDo6DQogOjogZGVzY3JpcHRpb24gMQ0KIDo6IGRlc2NyaXB0aW9uIDINCn19fQ0Ke3t7IyF3aWtpIGRhc2hlZA0KIHRlcm06OiBkZWZpbml0aW9uDQogb2JqZWN0OjoNCiA6OiBkZXNjcmlwdGlvbiAxDQogOjogZGVzY3JpcHRpb24gMg0KfX19DQoNCj09IEhvcml6b250YWwgUnVsZXMgPT0NCicnJycnc2VlOicnJyBIZWxwT25SdWxlcycnDQp7e3sNCi0tLS0NCi0tLS0tDQotLS0tLS0NCi0tLS0tLS0NCi0tLS0tLS0tDQotLS0tLS0tLS0NCi0tLS0tLS0tLS0NCn19fQ0Ke3t7IyF3aWtpIGRhc2hlZA0KLS0tLQ0KLS0tLS0NCi0tLS0tLQ0KLS0tLS0tLQ0KLS0tLS0tLS0NCi0tLS0tLS0tLQ0KLS0tLS0tLS0tLQ0KfX19DQoNCj09IFRhYmxlcyA9PQ0KJycnJydzZWU6JycnIEhlbHBPblRhYmxlcycnDQo9PT0gVGFibGVzID09PQ0Ke3t7DQp8fCcnJ0EnJyd8fCcnJ0InJyd8fCcnJ0MnJyd8fA0KfHwxICAgICAgfHwyICAgICAgfHwzICAgICAgfHwNCn19fQ0Ke3t7IyF3aWtpIGRhc2hlZA0KfHwnJydBJycnfHwnJydCJycnfHwnJydDJycnfHwNCnx8MSAgICAgIHx8MiAgICAgIHx8MyAgICAgIHx8DQp9fX0NCj09PSBDZWxsIFdpZHRoID09PQ0Ke3t7DQp8fG1pbmltYWwgd2lkdGggfHw8OTklPm1heGltYWwgd2lkdGggfHwNCn19fQ0Ke3t7IyF3aWtpIGRhc2hlZA0KfHxtaW5pbWFsIHdpZHRoIHx8PDk5JT5tYXhpbWFsIHdpZHRoIHx8DQp9fX0NCg0KPT09IFNwYW5uaW5nIFJvd3MgYW5kIENvbHVtbnMgID09PQ0Ke3t7DQp8fDx8Mj4gY2VsbCBzcGFubmluZyAyIHJvd3MgfHxjZWxsIGluIHRoZSAybmQgY29sdW1uIHx8DQp8fGNlbGwgaW4gdGhlIDJuZCBjb2x1bW4gb2YgdGhlIDJuZCByb3cgfHwNCnx8PC0yPiBjZWxsIHNwYW5uaW5nIDIgY29sdW1ucyB8fA0KfHx8fHVzZSBlbXB0eSBjZWxscyBhcyBhIHNob3J0aGFuZCB8fA0KfX19DQp7e3sjIXdpa2kgZGFzaGVkDQp8fDx8Mj4gY2VsbCBzcGFubmluZyAyIHJvd3MgfHxjZWxsIGluIHRoZSAybmQgY29sdW1uIHx8DQp8fGNlbGwgaW4gdGhlIDJuZCBjb2x1bW4gb2YgdGhlIDJuZCByb3cgfHwNCnx8PC0yPiBjZWxsIHNwYW5uaW5nIDIgY29sdW1ucyB8fA0KfHx8fHVzZSBlbXB0eSBjZWxscyBhcyBhIHNob3J0aGFuZCB8fA0KfX19DQoNCj09PSBBbGlnbm1lbnQgb2YgQ2VsbCBDb250ZW50cyA9PT0NCnt7ew0KfHw8XnwzPiB0b3AgKGNvbWJpbmVkKSB8fDw6OTklPiBjZW50ZXIgKGNvbWJpbmVkKSB8fDx2fDM+IGJvdHRvbSAoY29tYmluZWQpIHx8DQp8fDwpPiByaWdodCB8fA0KfHw8KD4gbGVmdCB8fA0KfX19DQp7e3sjIXdpa2kgZGFzaGVkDQp8fDxefDM+IHRvcCAoY29tYmluZWQpIHx8PDo5OSU+IGNlbnRlciAoY29tYmluZWQpIHx8PHZ8Mz4gYm90dG9tIChjb21iaW5lZCkgfHwNCnx8PCk+IHJpZ2h0IHx8DQp8fDwoPiBsZWZ0IHx8DQp9fX0NCg0KPT09IENvbG91cmVkIFRhYmxlIENlbGxzID09PQ0Ke3t7DQp8fDwjMDAwMEZGPiBibHVlIHx8PCMwMEZGMDA+IGdyZWVuICAgIHx8PCNGRjAwMDA+IHJlZCAgICB8fA0KfHw8IzAwRkZGRj4gY3lhbiB8fDwjRkYwMEZGPiBtYWdlbnRhICB8fDwjRkZGRjAwPiB5ZWxsb3cgfHwNCn19fQ0Ke3t7IyF3aWtpIGRhc2hlZA0KfHw8IzAwMDBGRj4gYmx1ZSB8fDwjMDBGRjAwPiBncmVlbiAgICB8fDwjRkYwMDAwPiByZWQgICAgfHwNCnx8PCMwMEZGRkY+IGN5YW4gfHw8I0ZGMDBGRj4gbWFnZW50YSAgfHw8I0ZGRkYwMD4geWVsbG93IHx8DQp9fX0NCg0KPT09IEhUTUwtbGlrZSBPcHRpb25zIGZvciBUYWJsZXMgPT09DQp7e3sNCnx8QSB8fDxyb3dzcGFuPSIyIj4gbGlrZSA8fDI+IHx8DQp8fDxiZ2NvbG9yPSIjMDBGRjAwIj4gbGlrZSA8IzAwRkYwMD4gfHwNCnx8PGNvbHNwYW49IjIiPiBsaWtlIDwtMj58fA0KfX19DQp7e3sjIXdpa2kgZGFzaGVkDQp8fEEgfHw8cm93c3Bhbj0iMiI+IGxpa2UgPHwyPiB8fA0KfHw8Ymdjb2xvcj0iIzAwRkYwMCI+IGxpa2UgPCMwMEZGMDA+IHx8DQp8fDxjb2xzcGFuPSIyIj4gbGlrZSA8LTI+fHwNCn19fQ0KDQo9PSBNYWNyb3MgYW5kIFZhcmlhYmxlcyA9PQ0KPT09IE1hY3JvcyA9PT0NCicnJycnc2VlOicnJyBIZWxwT25NYWNyb3MnJw0KICogPDxBbmNob3IoYW5jaG9ybmFtZSk+PmA8PEFuY2hvcihhbmNob3JuYW1lKT4+YCBpbnNlcnRzIGEgbGluayBhbmNob3IgYGFuY2hvcm5hbWVgDQogKiBgPDxCUj4+YCBpbnNlcnRzIGEgaGFyZCBsaW5lIGJyZWFrDQogKiBgPDxGb290Tm90ZShOb3RlKT4+YCBpbnNlcnRzIGEgZm9vdG5vdGUgc2F5aW5nIGBOb3RlYA0KICogYDw8SW5jbHVkZShIZWxwT25NYWNyb3MvSW5jbHVkZSk+PmAgaW5zZXJ0cyB0aGUgY29udGVudHMgb2YgdGhlIHBhZ2UgYEhlbHBPbk1hY3Jvcy9JbmNsdWRlYCBpbmxpbmUNCiAqIGA8PE1haWxUbyh1c2VyIEFUIGV4YW1wbGUgRE9UIGNvbSk+PmAgb2JmdXNjYXRlcyB0aGUgZW1haWwgYWRkcmVzcyBgdXNlckBleGFtcGxlLmNvbWAgdG8gdXNlcnMgbm90IGxvZ2dlZCBpbg0KDQo9PT0gVmFyaWFibGVzID09PQ0KJycnJydzZWU6JycnIEhlbHBPblZhcmlhYmxlcycnDQogKiBgQGBgU0lHYGBAYCBpbnNlcnRzIHlvdXIgbG9naW4gbmFtZSBhbmQgdGltZXN0YW1wIG9mIG1vZGlmaWNhdGlvbg0KICogYEBgYFRJTUVgYEBgIGluc2VydHMgZGF0ZSBhbmQgdGltZSBvZiBtb2RpZmljYXRpb24NCg0KPT0gU21pbGV5cyBhbmQgSWNvbnMgPT0NCicnJycnc2VlOicnJyBIZWxwT25TbWlsZXlzJycNCjw8U2hvd1NtaWxleXM+Pg0KDQo9PSBQYXJzZXJzID09DQonJycnJ3NlZTonJycgSGVscE9uUGFyc2VycycnDQoNCj09PSBWZXJiYXRpbSBEaXNwbGF5ID09PQ0Ke3t7ew0Ke3t7DQpkZWYgaGVsbG8oKToNCiAgICBwcmludCAiSGVsbG8gV29ybGQhIg0KfX19DQp9fX19DQoNCnt7ew0KZGVmIGhlbGxvKCk6DQogICAgcHJpbnQgIkhlbGxvIFdvcmxkISINCn19fQ0KDQo9PT0gU3ludGF4IEhpZ2hsaWdodGluZyA9PT0NCnt7e3sNCnt7eyMhaGlnaA==</chunk>
+<chunk>bGlnaHQgcHl0aG9uDQpkZWYgaGVsbG8oKToNCiAgICBwcmludCAiSGVsbG8gV29ybGQhIg0KfX19DQp9fX19DQoNCnt7eyMhaGlnaGxpZ2h0IHB5dGhvbg0KZGVmIGhlbGxvKCk6DQogICAgcHJpbnQgIkhlbGxvIFdvcmxkISINCn19fQ0KDQo9PT0gVXNpbmcgdGhlIHdpa2kgcGFyc2VyIHdpdGggY3NzIGNsYXNzZXMgPT09DQp7e3t7DQp7e3sjIXdpa2kgcmVkL3NvbGlkDQpUaGlzIGlzIHdpa2kgbWFya3VwIGluIGEgJycnZGl2JycnIHdpdGggX19jc3NfXyBgY2xhc3M9InJlZCBzb2xpZCJgLg0KfX19DQp9fX19DQoNCnt7eyMhd2lraSByZWQvc29saWQNClRoaXMgaXMgd2lraSBtYXJrdXAgaW4gYSAnJydkaXYnJycgd2l0aCBfX2Nzc19fIGBjbGFzcz0icmVkIHNvbGlkImAuDQp9fX0NCg0KPT0gQWRtb25pdGlvbnMgPT0NCicnJycnc2VlOicnJyBIZWxwT25BZG1vbml0aW9ucycnDQoNCnt7e3sNCnt7eyMhd2lraSBjYXV0aW9uDQonJydEb24ndCBvdmVydXNlIGFkbW9uaXRpb25zJycnDQoNCkFkbW9uaXRpb25zIHNob3VsZCBiZSB1c2VkIHdpdGggY2FyZS4gQSBwYWdlIHJpZGRsZWQgd2l0aCBhZG1vbml0aW9ucyB3aWxsIGxvb2sgcmVzdGxlc3MgYW5kIHdpbGwgYmUgaGFyZGVyIHRvIGZvbGxvdyB0aGFuIGEgcGFnZSB3aGVyZSBhZG1vbml0aW9ucyBhcmUgdXNlZCBzcGFyaW5nbHkuDQp9fX0NCn19fX0NCg0Ke3t7IyF3aWtpIGNhdXRpb24NCicnJ0Rvbid0IG92ZXJ1c2UgYWRtb25pdGlvbnMnJycNCg0KQWRtb25pdGlvbnMgc2hvdWxkIGJlIHVzZWQgd2l0aCBjYXJlLiBBIHBhZ2UgcmlkZGxlZCB3aXRoIGFkbW9uaXRpb25zIHdpbGwgbG9vayByZXN0bGVzcyBhbmQgd2lsbCBiZSBoYXJkZXIgdG8gZm9sbG93IHRoYW4gYSBwYWdlIHdoZXJlIGFkbW9uaXRpb25zIGFyZSB1c2VkIHNwYXJpbmdseS4NCn19fQ0KDQoNCj09IENvbW1lbnRzID09DQonJycnJ3NlZTonJycgSGVscE9uQ29tbWVudHMnJw0KDQp7e3sNCkNsaWNrIG9uICJDb21tZW50cyIgaW4gZWRpdCBiYXIgdG8gdG9nZ2xlIHRoZSAvKiBjb21tZW50cyAqLyB2aXNpYmlsaXR5Lg0KfX19DQp7e3sjIXdpa2kgZGFzaGVkDQpDbGljayBvbiAiQ29tbWVudHMiIGluIGVkaXQgYmFyIHRvIHRvZ2dsZSB0aGUgLyogY29tbWVudHMgKi8gdmlzaWJpbGl0eS4NCn19fQ0KDQoNCnt7e3sNCnt7eyMhd2lraSBjb21tZW50L2Rhc2hlZA0KVGhpcyBpcyBhIHdpa2kgcGFyc2VyIHNlY3Rpb24gd2l0aCBjbGFzcyAiY29tbWVudCBkYXNoZWQiIChzZWUgSGVscE9uUGFyc2VycykuDQoNCkl0cyB2aXNpYmlsaXR5IGdldHMgdG9nZ2xlZCB0aGUgc2FtZSB3YXkuDQp9fX0NCn19fX0NCg0Ke3t7IyF3aWtpIGNvbW1lbnQvZGFzaGVkDQpUaGlzIGlzIGEgd2lraSBwYXJzZXIgc2VjdGlvbiB3aXRoIGNsYXNzICJjb21tZW50IGRhc2hlZCIgKHNlZSBIZWxwT25QYXJzZXJzKS4NCg0KSXRzIHZpc2liaWxpdHkgZ2V0cyB0b2dnbGVkIHRoZSBzYW1lIHdheS4NCn19fQ0K</chunk>
+</data>
+</revision>
+</item>
+</backend>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/wiki/data/plugin/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,3 @@
+"""
+This file was added so as to make hg track the folders necessary for the tests.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/wikiconfig.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,25 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin - test wiki configuration
+
+Do not change any values without good reason.
+
+We mostly want to have default values here, except for stuff that doesn't
+work without setting them (like data_dir).
+
+@copyright: 2000-2004 by Juergen Hermann <jh@web.de>
+@license: GNU GPL, see COPYING for details.
+"""
+
+import os
+from os.path import abspath, dirname, join
+from MoinMoin.config.default import DefaultConfig
+
+class Config(DefaultConfig):
+    _here = abspath(dirname(__file__))
+    _root = abspath(join(_here, '..', '..'))
+    data_dir = join(_here, 'wiki', 'data') # needed for plugins package TODO
+    _test_items_xml = join(_here, 'testitems.xml')
+    content_acl = None
+    item_root = 'FrontPage'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/app.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,301 @@
+# -*- coding: ascii -*-
+"""
+MoinMoin - wsgi application setup and related code
+
+Use create_app(config) to create the WSGI application (using Flask).
+
+@copyright: 2000-2006 by Juergen Hermann <jh@web.de>,
+            2002-2011 MoinMoin:ThomasWaldmann,
+            2008 MoinMoin:FlorianKrupicka,
+            2010 MoinMoin:DiogenesAugusto
+@license: GNU GPL, see COPYING for details.
+"""
+import os
+
+# do this early, but not in MoinMoin/__init__.py because we need to be able to
+# "import MoinMoin" from setup.py even before flask, werkzeug, ... is installed.
+from MoinMoin.util import monkeypatch
+
+from flask import Flask, request, session, flaskg
+from flask import current_app as app
+
+from flaskext.cache import Cache
+from flaskext.themes import setup_themes
+
+from werkzeug.exceptions import HTTPException
+
+from jinja2 import ChoiceLoader, FileSystemLoader
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin.i18n import i18n_init
+from MoinMoin.i18n import _, L_, N_
+
+from MoinMoin.themes import setup_jinja_env, themed_error
+
+def create_app(config=None):
+    """simple wrapper around create_app_ext() for flask-script"""
+    return create_app_ext(flask_config_file=config)
+
+
+def create_app_ext(flask_config_file=None, flask_config_dict=None,
+                   moin_config_class=None, warn_default=True, **kwargs
+                  ):
+    """
+    Factory for moin wsgi apps
+
+    @param flask_config_file: a flask config file name (may have a MOINCFG class),
+                              if not given, a config pointed to by MOINCFG env var
+                              will be loaded (if possible).
+    @param flask_config_dict: a dict used to update flask config (applied after
+                              flask_config_file was loaded [if given])
+    @param moin_config_class: if you give this, it'll be instantiated as app.cfg,
+                              otherwise it'll use MOINCFG from flask config. If that
+                              also is not there, it'll use the DefaultConfig built
+                              into MoinMoin.
+    @oaram warn_default: emit a warning if moin falls back to its builtin default
+                         config (maybe user forgot to specify MOINCFG?)
+    @param **kwargs: if you give additional key/values here, they'll get patched
+                     into the moin configuration class (before it instance is created)
+    """
+    clock = Clock()
+    clock.start('create_app total')
+    app = Flask('MoinMoin')
+    clock.start('create_app load config')
+    if flask_config_file:
+        app.config.from_pyfile(flask_config_file)
+    else:
+        app.config.from_envvar('MOINCFG', silent=True)
+    if flask_config_dict:
+        app.config.update(flask_config_dict)
+    Config = moin_config_class
+    if not Config:
+        Config = app.config.get('MOINCFG')
+    if not Config:
+        if warn_default:
+            logging.warning("using builtin default configuration")
+        from MoinMoin.config.default import DefaultConfig as Config
+    for key, value in kwargs.iteritems():
+        setattr(Config, key, value)
+    if Config.secrets is None:
+        # reuse the secret configured for flask (which is required for sessions)
+        Config.secrets = app.config.get('SECRET_KEY')
+    app.cfg = Config()
+    clock.stop('create_app load config')
+    clock.start('create_app register')
+    # register converters
+    from werkzeug.routing import PathConverter
+    app.url_map.converters['itemname'] = PathConverter
+    # register modules, before/after request functions
+    from MoinMoin.apps.frontend import frontend
+    frontend.before_request(before_wiki)
+    frontend.after_request(after_wiki)
+    app.register_module(frontend)
+    from MoinMoin.apps.admin import admin
+    admin.before_request(before_wiki)
+    admin.after_request(after_wiki)
+    app.register_module(admin, url_prefix='/+admin')
+    from MoinMoin.apps.feed import feed
+    feed.before_request(before_wiki)
+    feed.after_request(after_wiki)
+    app.register_module(feed, url_prefix='/+feed')
+    from MoinMoin.apps.misc import misc
+    misc.before_request(before_wiki)
+    misc.after_request(after_wiki)
+    app.register_module(misc, url_prefix='/+misc')
+    from MoinMoin.apps.serve import serve
+    app.register_module(serve, url_prefix='/+serve')
+    clock.stop('create_app register')
+    clock.start('create_app flask-cache')
+    cache = Cache()
+    cache.init_app(app)
+    app.cache = cache
+    clock.stop('create_app flask-cache')
+    # init storage
+    clock.start('create_app init backends')
+    app.unprotected_storage, app.storage = init_backends(app)
+    clock.stop('create_app init backends')
+    clock.start('create_app index rebuild')
+    if app.cfg.index_rebuild:
+        app.unprotected_storage.index_rebuild() # XXX run this from a script
+    clock.stop('create_app index rebuild')
+    clock.start('create_app load/save xml')
+    import_export_xml(app)
+    clock.stop('create_app load/save xml')
+    clock.start('create_app flask-babel')
+    i18n_init(app)
+    clock.stop('create_app flask-babel')
+    # configure templates
+    clock.start('create_app flask-themes')
+    setup_themes(app)
+    if app.cfg.template_dirs:
+        app.jinja_env.loader = ChoiceLoader([
+            FileSystemLoader(app.cfg.template_dirs),
+            app.jinja_env.loader,
+        ])
+    app.error_handlers[403] = themed_error
+    clock.stop('create_app flask-themes')
+    clock.stop('create_app total')
+    del clock
+    return app
+
+
+from MoinMoin.util.clock import Clock
+from MoinMoin.storage.error import StorageError
+from MoinMoin.storage.serialization import serialize, unserialize
+from MoinMoin.storage.backends import router, acl, memory
+from MoinMoin import auth, config, user
+
+
+def set_umask(new_mask=0777^config.umask):
+    """ Set the OS umask value (and ignore potential failures on OSes where
+        this is not supported).
+        Default: the bitwise inverted value of config.umask
+    """
+    try:
+        old_mask = os.umask(new_mask)
+    except:
+        # maybe we are on win32?
+        pass
+
+
+def init_backends(app):
+    """ initialize the backend """
+    # A ns_mapping consists of several lines, where each line is made up like this:
+    # mountpoint, unprotected backend, protection to apply as a dict
+    ns_mapping = app.cfg.namespace_mapping
+    index_uri = app.cfg.router_index_uri
+    # Just initialize with unprotected backends.
+    unprotected_mapping = [(ns, backend) for ns, backend, acls in ns_mapping]
+    unprotected_storage = router.RouterBackend(unprotected_mapping, index_uri=index_uri)
+    # Protect each backend with the acls provided for it in the mapping at position 2
+    amw = acl.AclWrapperBackend
+    protected_mapping = [(ns, amw(app.cfg, backend, **acls)) for ns, backend, acls in ns_mapping]
+    storage = router.RouterBackend(protected_mapping, index_uri=index_uri)
+    return unprotected_storage, storage
+
+
+def import_export_xml(app):
+    # If the content was already pumped into the backend, we don't want
+    # to do that again. (Works only until the server is restarted.)
+    xmlfile = app.cfg.load_xml
+    if xmlfile:
+        app.cfg.load_xml = None
+        tmp_backend = router.RouterBackend([('/', memory.MemoryBackend())],
+                                           index_uri='sqlite://')
+        unserialize(tmp_backend, xmlfile)
+        # TODO optimize this, maybe unserialize could count items it processed
+        item_count = 0
+        for item in tmp_backend.iteritems():
+            item_count += 1
+        logging.debug("loaded xml into tmp_backend: %s, %d items" % (xmlfile, item_count))
+        try:
+            # In case the server was restarted we cannot know whether
+            # the xml data already exists in the target backend.
+            # Hence we check the existence of the items before we unserialize
+            # them to the backend.
+            backend = app.unprotected_storage
+            for item in tmp_backend.iteritems():
+                item = backend.get_item(item.name)
+        except StorageError:
+            # if there is some exception, we assume that backend needs to be filled
+            # we need to use it as unserialization target so that update mode of
+            # unserialization creates the correct item revisions
+            logging.debug("unserialize xml file %s into %r" % (xmlfile, backend))
+            unserialize(backend, xmlfile)
+    else:
+        item_count = 0
+
+    # XXX wrong place / name - this is a generic preload functionality, not just for tests
+    # To make some tests happy
+    app.cfg.test_num_pages = item_count
+
+    xmlfile = app.cfg.save_xml
+    if xmlfile:
+        app.cfg.save_xml = None
+        backend = app.unprotected_storage
+        serialize(backend, xmlfile)
+
+
+def setup_user():
+    """ Try to retrieve a valid user object from the request, be it
+    either through the session or through a login. """
+    # init some stuff for auth processing:
+    flaskg._login_multistage = None
+    flaskg._login_multistage_name = None
+    flaskg._login_messages = []
+
+    # first try setting up from session
+    userobj = auth.setup_from_session()
+
+    # then handle login/logout forms
+    form = request.values.to_dict()
+    if 'login_submit' in form:
+        # this is a real form, submitted by POST
+        userobj = auth.handle_login(userobj, **form)
+    elif 'logout_submit' in form:
+        # currently just a GET link
+        userobj = auth.handle_logout(userobj)
+    else:
+        userobj = auth.handle_request(userobj)
+
+    # if we still have no user obj, create a dummy:
+    if not userobj:
+        userobj = user.User(auth_method='invalid')
+    # if we have a valid user we store it in the session
+    if userobj.valid:
+        session['user.id'] = userobj.id
+        session['user.auth_method'] = userobj.auth_method
+        session['user.auth_attribs'] = userobj.auth_attribs
+    return userobj
+
+
+def before_wiki():
+    """
+    Setup environment for wiki requests, start timers.
+    """
+    logging.debug("running before_wiki")
+    flaskg.clock = Clock()
+    flaskg.clock.start('total')
+    flaskg.clock.start('init')
+    try:
+        set_umask() # do it once per request because maybe some server
+                    # software sets own umask
+
+        flaskg.unprotected_storage = app.unprotected_storage
+
+        try:
+            flaskg.user = setup_user()
+        except HTTPException, e:
+            # this makes stuff like abort(redirect(...)) work
+            return app.handle_http_exception(e)
+
+        flaskg.dicts = app.cfg.dicts()
+        flaskg.groups = app.cfg.groups()
+
+        flaskg.content_lang = app.cfg.language_default
+        flaskg.current_lang = app.cfg.language_default
+
+        flaskg.storage = app.storage
+
+        setup_jinja_env()
+    finally:
+        flaskg.clock.stop('init')
+
+    # if return value is not None, it is the final response
+
+
+def after_wiki(response):
+    """
+    Stop timers.
+    """
+    logging.debug("running after_wiki")
+    try:
+        flaskg.clock.stop('total')
+        del flaskg.clock
+    except AttributeError:
+        # can happen if after_wiki() is called twice, e.g. by unit tests.
+        pass
+    return response
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,14 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - admin views package
+
+    This package contains all views, templates, static files for wiki administration.
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from flask import Module
+admin = Module(__name__)
+import MoinMoin.apps.admin.views
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/index.html	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,9 @@
+{% extends theme("layout.html") %}
+{% block content %}
+<h1>{{ _("Admin Menu") }}</h1>
+<ul>
+    <li><a href="{{ url_for('admin.userbrowser') }}">{{ _("User Browser") }}</a></li>
+    <li><a href="{{ url_for('admin.sysitems_upgrade') }}">{{ _("Upgrade system items") }}</a></li>
+</ul>
+{% endblock %}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/sysitems_upgrade.html	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,14 @@
+{% extends theme("layout.html") %}
+{% block content %}
+<h1>{{ _("System items upgrade") }}</h1>
+<p>
+{{ _("You can upgrade your system items by uploading an xml file with new items below.") }}
+</p>
+<form action="{{ url_for('admin.sysitems_upgrade') }}" method="POST" enctype="multipart/form-data">
+<fieldset>
+    <label for="xmlfile">System items XML file:</label><input type="file" id="xmlfile" name="xmlfile" />
+    <input type="submit" name="submit" value="{{ _("Upgrade system items") }}" />
+</fieldset>
+</form>
+{% endblock %}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/userbrowser.html	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,35 @@
+{% extends theme("layout.html") %}
+{% block content %}
+    <table>
+    <tr>
+        <th>{{ _("User name") }}</th>
+        <th>{{ _("Member of Groups") }}</th>
+        <th>{{ _("Email address") }}</th>
+        <th>{{ _("Actions") }}</th>
+    </tr>
+    {% for u in user_accounts %}
+    <tr>
+        <td><a href="{{ url_for('frontend.show_item', item_name=u.name) }}">{{ u.name }}</a>{{ u.disabled and " (%s)" % _("disabled") or ""}}</td>
+        <td>{{ u.groups|join(',') }}</td>
+        <td>
+            {% if u.email %}
+            <a href="mailto:{{ u.email|e }}" class="mailto">{{ u.email|e }}</a>
+            {% endif %}
+        </td>
+        <td>
+            <form action="{{ url_for('admin.userprofile', user_name=u.name) }}" method="GET">
+                <input type="hidden" name="key" value="disabled" />
+                <input type="hidden" name="val" value="{{ u.disabled and "0" or "1" }}" />
+                <input type="submit" name="userprofile" value="{{ u.disabled and _("Enable user") or _("Disable user") }}" />
+            </form>
+            <form action="{{ url_for('admin.mail_recovery_token') }}" method="GET">
+                <input type="hidden" name="email" value="{{ u.email }}" />
+                <input type="hidden" name="account_sendmail" value="1" />
+                <input type="submit" name="recoverpass" value="{{ _("Mail account data") }}" />
+            </form>
+        </td>
+    </tr>
+    {% endfor %}
+</table>
+{% endblock %}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/views.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,111 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - admin views
+
+    This shows the user interface for wiki admins.
+
+    @copyright: 2008-2010 MoinMoin:ThomasWaldmann,
+                2001-2003 Juergen Hermann <jh@web.de>,
+                2010 MoinMoin:DiogenesAugusto,
+                2010 MoinMoin:ReimarBauer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from flask import request, url_for, flash, redirect
+from flask import current_app as app
+from flask import flaskg
+
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.themes import render_template
+from MoinMoin.apps.admin import admin
+from MoinMoin import user
+
+@admin.route('/')
+def index():
+    return render_template('admin/index.html', item_name="+admin")
+
+
+@admin.route('/userbrowser')
+def userbrowser():
+    """
+    User Account Browser
+    """
+    # XXX add superuser check
+    groups = flaskg.groups
+    user_accounts = []
+    for uid in user.getUserList():
+        u = user.User(uid)
+        user_accounts.append(dict(
+            uid=uid,
+            name=u.name,
+            email=u.email,
+            disabled=u.disabled,
+            groups=[groupname for groupname in groups if u.name in groups[groupname]],
+            ))
+    return render_template('admin/userbrowser.html', user_accounts=user_accounts, item_name="+admin/Userbrowser")
+
+
+@admin.route('/userprofile/<user_name>', methods=['GET', 'POST', ])
+def userprofile(user_name):
+    """
+    Set values in user profile
+    """
+    # XXX add superuser check
+    uid = user.getUserId(user_name)
+    u = user.User(uid)
+    if request.method == 'GET':
+        return _(u"User profile of %(username)s: %(email)r", username=user_name,
+                 email=(u.email, u.disabled))
+
+    if request.method == 'POST':
+        key = request.form.get('key', '')
+        val = request.form.get('val', '')
+        ok = False
+        if hasattr(u, key):
+            ok = True
+            oldval = getattr(u, key)
+            if isinstance(oldval, bool):
+                val = bool(val)
+            elif isinstance(oldval, int):
+                val = int(val)
+            elif isinstance(oldval, unicode):
+                val = unicode(val)
+            else:
+                ok = False
+        if ok:
+            setattr(u, key, val)
+            theuser.save()
+            flash('%s.%s: %s -> %s' % (user_name, key, unicode(oldval), unicode(val), ), "info")
+        else:
+            flash('modifying %s.%s failed' % (user_name, key, ), "error")
+    return redirect(url_for('admin.userbrowser'))
+
+
+@admin.route('/mail_recovery_token', methods=['GET', 'POST', ])
+def mail_recovery_token():
+    """
+    Send user an email so he can reset his password.
+    """
+    flash("mail recovery token not implemented yet")
+    return redirect(url_for('admin.userbrowser'))
+
+
+@admin.route('/sysitems_upgrade', methods=['GET', 'POST', ])
+def sysitems_upgrade():
+    from MoinMoin.storage.backends import upgrade_sysitems
+    from MoinMoin.storage.error import BackendError
+    if request.method == 'GET':
+        action = 'syspages_upgrade'
+        label = 'Upgrade System Pages'
+        return render_template('admin/sysitems_upgrade.html',
+                               item_name="+admin/System items upgrade")
+    if request.method == 'POST':
+        xmlfile = request.files.get('xmlfile')
+        try:
+            upgrade_sysitems(xmlfile)
+        except BackendError, e:
+            flash(_('System items upgrade failed due to the following error: %(error)s.', error=e), 'error')
+        else:
+            flash(_('System items have been upgraded successfully!'))
+        return redirect(url_for('admin.index'))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/feed/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,15 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - feed views package
+
+    This package contains all views, templates, static files for feeds
+    (like atom, ...).
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from flask import Module
+feed = Module(__name__)
+import MoinMoin.apps.feed.views
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/feed/_tests/test_feed.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - basic tests for feeds
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+class TestFeeds(object):
+    def test_global_atom(self):
+        with self.app.test_client() as c:
+            rv = c.get('/+feed/atom')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'application/atom+xml'
+            assert rv.data.startswith('<?xml')
+            assert '<feed xmlns="http://www.w3.org/2005/Atom">' in rv.data
+            assert '</feed>' in rv.data
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/feed/views.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,75 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - feed views
+
+    This contains all sort of feeds.
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+                2010 MoinMoin:DiogenesAugusto
+@license: GNU GPL, see COPYING for details.
+"""
+
+from datetime import datetime
+
+from flask import request, url_for, Response
+from flask import flaskg
+
+from flask import current_app as app
+
+from werkzeug.contrib.atom import AtomFeed
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin import wikiutil
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.apps.feed import feed
+from MoinMoin.items import NAME, ACL, MIMETYPE, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT
+from MoinMoin.themes import get_editor_info
+from MoinMoin.items import Item
+
+@feed.route('/atom/<itemname:item_name>')
+@feed.route('/atom', defaults=dict(item_name=''))
+def atom(item_name):
+    # maybe we need different modes:
+    # - diffs in html don't look great without stylesheet
+    # - full item in html is nice
+    # - diffs in textmode are OK, but look very simple
+    # - full-item content in textmode is OK, but looks very simple
+    cid = wikiutil.cache_key(usage="atom", item_name=item_name)
+    content = app.cache.get(cid)
+    if content is None:
+        title = app.cfg.sitename
+        feed = AtomFeed(title=title, feed_url=request.url, url=request.host_url)
+        for rev in flaskg.storage.history(item_name=item_name):
+            this_rev = rev
+            this_revno = rev.revno
+            item = rev.item
+            name = rev[NAME]
+            try:
+                hl_item = Item.create(name, rev_no=this_revno)
+                previous_revno = this_revno - 1
+                if previous_revno >= 0:
+                    # simple text diff for changes
+                    previous_rev = item.get_revision(previous_revno)
+                    content = hl_item._render_data_diff_text(previous_rev, this_rev)
+                    content = '<div><pre>%s</pre></div>' % content
+                else:
+                    # full html rendering for new items
+                    content = hl_item._render_data()
+                content_type = 'xhtml'
+            except Exception, e:
+                logging.exception("content rendering crashed")
+                content = _(u'MoinMoin feels unhappy.')
+                content_type = 'text'
+            feed.add(title=name, title_type='text',
+                     summary=rev.get(COMMENT, ''), summary_type='text',
+                     content=content, content_type=content_type,
+                     author=get_editor_info(rev, external=True),
+                     url=url_for('frontend.show_item', item_name=name, rev=this_revno, _external=True),
+                     updated=datetime.utcfromtimestamp(rev.timestamp),
+                    )
+        content = feed.to_string()
+        app.cache.set(cid, content)
+    return Response(content, content_type='application/atom+xml')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/frontend/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,15 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - frontend views package
+
+    This package contains all views, templates, static files that a normal wiki
+    user usually sees.
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from flask import Module
+frontend = Module(__name__)
+import MoinMoin.apps.frontend.views
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/frontend/_tests/test_frontend.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - basic tests for frontend
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.apps.frontend import views
+from werkzeug import ImmutableMultiDict
+from flask import flaskg
+from MoinMoin import user
+
+class TestFrontend(object):
+    def test_root(self):
+        with self.app.test_client() as c:
+            rv = c.get('/') # / redirects to front page
+            assert rv.status == '302 FOUND'
+
+    def test_robots(self):
+        with self.app.test_client() as c:
+            rv = c.get('/robots.txt')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'text/plain; charset=utf-8'
+            assert 'Disallow:' in rv.data
+
+    def test_favicon(self):
+        with self.app.test_client() as c:
+            rv = c.get('/favicon.ico')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'image/x-icon'
+            assert rv.data.startswith('\x00\x00') # "reserved word, should always be 0"
+
+    def test_404(self):
+        with self.app.test_client() as c:
+            rv = c.get('/DoesntExist')
+            assert rv.status == '404 NOT FOUND'
+            assert rv.headers['Content-Type'] == 'text/html; charset=utf-8'
+            assert '<html>' in rv.data
+            assert '</html>' in rv.data
+
+    def test_global_index(self):
+        with self.app.test_client() as c:
+            rv = c.get('/+index')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'text/html; charset=utf-8'
+            assert '<html>' in rv.data
+            assert '</html>' in rv.data
+
+class TestUsersettings(object):
+    def setup_method(self, method):
+        # Save original user
+        self.saved_user = flaskg.user
+
+        # Create anon user for the tests
+        flaskg.user = user.User()
+
+        self.user = None
+
+    def teardown_method(self, method):
+        """ Run after each test
+
+        Remove user and reset user listing cache.
+        """
+        # Remove user file and user
+        if self.user is not None:
+            del self.user
+
+        # Restore original user
+        flaskg.user = self.saved_user
+
+    def test_user_password_change(self):
+        self.createUser(u'moin', u'Xiwejr622')
+        flaskg.user = user.User(name=u'moin', password=u'Xiwejr622')
+        form = self.fillPasswordChangeForm(u'Xiwejr622', u'Woodoo645', u'Woodoo645')
+        valid = form.validate()
+        assert valid # form data is valid
+
+    def test_user_unicode_password_change(self):
+        name = u'moin'
+        password = u'__שם משתמש לא קיים__' # Hebrew
+
+        self.createUser(name, password)
+        flaskg.user = user.User(name=name, password=password)
+        form = self.fillPasswordChangeForm(password, u'Woodoo645', u'Woodoo645')
+        valid = form.validate()
+        assert valid # form data is valid
+
+    def test_user_password_change_to_unicode_pw(self):
+        name = u'moin'
+        password = u'Xiwejr622'
+        new_password = u'__שם משתמש לא קיים__' # Hebrew
+
+        self.createUser(name, password)
+        flaskg.user = user.User(name=name, password=password)
+        form = self.fillPasswordChangeForm(password, new_password, new_password)
+        valid = form.validate()
+        assert valid # form data is valid
+
+    def test_fail_user_password_change_pw_mismatch(self):
+        self.createUser(u'moin', u'Xiwejr622')
+        flaskg.user = user.User(name=u'moin', password=u'Xiwejr622')
+        form = self.fillPasswordChangeForm(u'Xiwejr622', u'Piped33', u'Woodoo645')
+        valid = form.validate()
+        # form data is invalid because password1 != password2
+        assert not valid
+
+    def test_fail_password_change(self):
+        self.createUser(u'moin', u'Xiwejr622')
+        flaskg.user = user.User(name=u'moin', password=u'Xiwejr622')
+        form = self.fillPasswordChangeForm(u'Xinetd33', u'Woodoo645', u'Woodoo645')
+        valid = form.validate()
+        # form data is invalid because password_current != user.password
+        assert not valid
+
+    # Helpers ---------------------------------------------------------
+
+    def fillPasswordChangeForm(self, current_password, password1, password2):
+        """ helper to fill UserSettingsPasswordForm form
+        """
+        FormClass = views.UserSettingsPasswordForm
+        request_form = ImmutableMultiDict(
+           [
+              ('usersettings_password_password_current', current_password),
+              ('usersettings_password_password1', password1),
+              ('usersettings_password_password2', password2),
+              ('usersettings_password_submit', u'Save')
+           ]
+        )
+        form = FormClass.from_flat(request_form)
+        return form
+
+    def createUser(self, name, password, pwencoded=False, email=None):
+        """ helper to create test user
+        """
+        # Create user
+        self.user = user.User()
+        self.user.name = name
+        self.user.email = email
+        if not pwencoded:
+            password = user.encodePassword(password)
+        self.user.enc_password = password
+
+        # Validate that we are not modifying existing user data file!
+        if self.user.exists():
+            self.user = None
+            py.test.skip("Test user exists, will not override existing user data file!")
+
+        # Save test user
+        self.user.save()
+
+        # Validate user creation
+        if not self.user.exists():
+            self.user = None
+            py.test.skip("Can't create test user")
+
+
+class TestViews(object):
+    """
+    Tester class for +backrefs, +orphans and +wanted views
+    """
+    class DummyItem(object):
+        """
+        Fake storage object, simulating the page item object from the storage
+        """
+        def __init__(self, name, revision):
+            self.latest_revision = revision
+            self.name = name
+
+        def get_revision(self, *args, **kw):
+            return self.latest_revision
+
+    class DummyRevision(object):
+        """
+        Fake revision object, used for retrieving ITEMTRANSCLUSIONS and ITEMLINKS meta
+        """
+        def __init__(self, links, transclusions):
+            self.links = links
+            self.transclusions = transclusions
+
+        def get(self, meta_name, *args, **kw):
+            if meta_name == 'itemlinks':
+                return self.links
+            if meta_name == 'itemtransclusions':
+                return self.transclusions
+
+    def setup_class(self):
+        # list of tuples
+        # (page_name, links, transclusions)
+        items = [('page1', ['page2', 'page3'], ['page2']),
+                 ('page2',  ['page1', 'page3'], []),
+                 ('page3', ['page5'], ['page1']),
+                 ('page4', [], ['page5'])
+                ]
+        # we create the list of items
+        self.items = []
+        for item in items:
+            revision = self.DummyRevision(item[1], item[2])
+            page = self.DummyItem(item[0], revision)
+            self.items.append(page)
+
+    def test_orphans(self):
+        expected_orphans = sorted(['page4'])
+        result_orphans = sorted(views._orphans(self.items))
+
+        assert result_orphans == expected_orphans
+
+    def test_wanteds(self):
+        expected_wanteds = {'page5': ['page3', 'page4']}
+        result_wanteds = views._wanteds(self.items)
+
+        assert result_wanteds == expected_wanteds
+
+    def test_backrefs(self):
+        expected_backrefs = sorted(['page1', 'page2'])
+        result_backrefs = sorted(views._backrefs(self.items, 'page3'))
+
+        assert result_backrefs == expected_backrefs
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/frontend/views.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,1602 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - frontend views
+
+    This shows the usual things users see when using the wiki.
+
+    @copyright: 2003-2010 MoinMoin:ThomasWaldmann,
+                2008 MoinMoin:FlorianKrupicka,
+                2010 MoinMoin:DiogenesAugusto
+@license: GNU GPL, see COPYING for details.
+"""
+
+import re
+import difflib
+import time
+from itertools import chain
+
+from flask import request, url_for, flash, Response, redirect, session, abort
+from flask import flaskg
+from flask import current_app as app
+from flaskext.themes import get_themes_list
+
+from flatland import Form, String, Integer, Boolean, Enum
+from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted
+
+from jinja2 import Markup
+
+import pytz
+from babel import Locale
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.themes import render_template
+from MoinMoin.apps.frontend import frontend
+from MoinMoin.items import Item, NonExistent, MIMETYPE, ITEMLINKS, ITEMTRANSCLUSIONS
+from MoinMoin.items import ROWS_META, COLS, ROWS_DATA
+from MoinMoin import config, user, wikiutil
+from MoinMoin.util.forms import make_generator
+from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
+from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, AccessDeniedError
+from MoinMoin.signalling import item_displayed, item_modified
+
+
+@frontend.route('/+dispatch', methods=['GET', ])
+def dispatch():
+    args = request.values.to_dict()
+    endpoint = str(args.pop('endpoint'))
+    return redirect(url_for(endpoint, **args))
+
+
+@frontend.route('/')
+def show_root():
+    item_name = app.cfg.item_root
+    location = url_for('frontend.show_item', item_name=item_name)
+    return redirect(location)
+
+@frontend.route('/robots.txt')
+def robots():
+    return Response("""\
+User-agent: *
+Crawl-delay: 20
+Disallow: /+convert/
+Disallow: /+dom/
+Disallow: /+modify/
+Disallow: /+copy/
+Disallow: /+delete/
+Disallow: /+destroy/
+Disallow: /+rename/
+Disallow: /+revert/
+Disallow: /+index/
+Disallow: /+sitemap/
+Disallow: /+similar_names/
+Disallow: /+quicklink/
+Disallow: /+subscribe/
+Disallow: /+backrefs/
+Disallow: /+wanteds/
+Disallow: /+orphans/
+Disallow: /+register
+Disallow: /+recoverpass
+Disallow: /+usersettings
+Disallow: /+login
+Disallow: /+logout
+Disallow: /+bookmark
+Disallow: /+diffsince/
+Disallow: /+diff/
+Disallow: /+diffraw/
+Disallow: /+dispatch/
+Disallow: /+admin/
+Allow: /
+""", mimetype='text/plain')
+
+
+@frontend.route('/favicon.ico')
+def favicon():
+    # although we tell that favicon.ico is at /static/logos/favicon.ico,
+    # some browsers still request it from /favicon.ico...
+    return app.send_static_file('logos/favicon.ico')
+
+
+@frontend.route('/<itemname:item_name>', defaults=dict(rev=-1))
+@frontend.route('/+show/<int:rev>/<itemname:item_name>')
+def show_item(item_name, rev):
+    flaskg.user.addTrail(item_name)
+    item_displayed.send(app._get_current_object(),
+                        item_name=item_name)
+    try:
+        item = Item.create(item_name, rev_no=rev)
+        rev_nos = item.rev.item.list_revisions()
+    except AccessDeniedError:
+        abort(403)
+    if rev_nos:
+        first_rev = rev_nos[0]
+        last_rev = rev_nos[-1]
+    else:
+        # Note: rev.revno of DummyRev is None
+        first_rev = None
+        last_rev = None
+    if isinstance(item, NonExistent):
+        status = 404
+    else:
+        status = 200
+    content = render_template('show.html',
+                              item=item, item_name=item.name,
+                              rev=item.rev,
+                              mimetype=item.mimetype,
+                              first_rev_no=first_rev,
+                              last_rev_no=last_rev,
+                              data_rendered=Markup(item._render_data()),
+                              show_navigation=(rev>=0),
+                             )
+    return Response(content, status)
+
+
+@frontend.route('/+show/<itemname:item_name>')
+def redirect_show_item(item_name):
+    return redirect(url_for('frontend.show_item', item_name=item_name))
+
+
+@frontend.route('/+dom/<int:rev>/<itemname:item_name>')
+@frontend.route('/+dom/<itemname:item_name>', defaults=dict(rev=-1))
+def show_dom(item_name, rev):
+    try:
+        item = Item.create(item_name, rev_no=rev)
+    except AccessDeniedError:
+        abort(403)
+    if isinstance(item, NonExistent):
+        status = 404
+    else:
+        status = 200
+    content = render_template('dom.xml',
+                              data_xml=item._render_data_xml(),
+                             )
+    return Response(content, status, mimetype='text/xml')
+
+
+@frontend.route('/+meta/<itemname:item_name>', defaults=dict(rev=-1))
+@frontend.route('/+meta/<int:rev>/<itemname:item_name>')
+def show_item_meta(item_name, rev):
+    flaskg.user.addTrail(item_name)
+    try:
+        item = Item.create(item_name, rev_no=rev)
+    except AccessDeniedError:
+        abort(403)
+    rev_nos = item.rev.item.list_revisions()
+    if rev_nos:
+        first_rev = rev_nos[0]
+        last_rev = rev_nos[-1]
+    else:
+        # Note: rev.revno of DummyRev is None
+        first_rev = None
+        last_rev = None
+    return render_template('meta.html',
+                           item=item, item_name=item.name,
+                           rev=item.rev,
+                           mimetype=item.mimetype,
+                           first_rev_no=first_rev,
+                           last_rev_no=last_rev,
+                           meta_rendered=Markup(item._render_meta()),
+                           show_navigation=(rev>=0),
+                          )
+
+
+@frontend.route('/+get/<int:rev>/<itemname:item_name>')
+@frontend.route('/+get/<itemname:item_name>', defaults=dict(rev=-1))
+def get_item(item_name, rev):
+    try:
+        item = Item.create(item_name, rev_no=rev)
+    except AccessDeniedError:
+        abort(403)
+    return item.do_get()
+
+@frontend.route('/+convert/<itemname:item_name>')
+def convert_item(item_name):
+    """
+    return a converted item.
+
+    We create two items : the original one, and an empty
+    one with the expected mimetype for the converted item.
+
+    To get the converted item, we just feed his converter,
+    with the internal representation of the item.
+    """
+    mimetype = request.values.get('mimetype')
+    try:
+        item = Item.create(item_name, rev_no=-1)
+    except AccessDeniedError:
+        abort(403)
+    # We don't care about the name of the converted object
+    # It should just be a name which does not exist.
+    # XXX Maybe use a random name to be sure it does not exist
+    item_name_converted = item_name + 'converted'
+    try:
+        converted_item = Item.create(item_name_converted, mimetype=mimetype)
+    except AccessDeniedError:
+        abort(403)
+    return converted_item._convert(item.internal_representation())
+
+@frontend.route('/+highlight/<int:rev>/<itemname:item_name>')
+@frontend.route('/+highlight/<itemname:item_name>', defaults=dict(rev=-1))
+def highlight_item(item_name, rev):
+    from MoinMoin.items import Text, NonExistent
+    from MoinMoin.util.tree import html
+    try:
+        item = Item.create(item_name, rev_no=rev)
+    except AccessDeniedError:
+        abort(403)
+    if isinstance(item, Text):
+        from MoinMoin.converter import default_registry as reg
+        from MoinMoin.util.mime import Type, type_moin_document
+        data_text = item.data_storage_to_internal(item.data)
+        # TODO: use registry as soon as it is in there
+        from MoinMoin.converter.pygments_in import Converter as PygmentsConverter
+        pygments_conv = PygmentsConverter(mimetype=item.mimetype)
+        doc = pygments_conv(data_text.split(u'\n'))
+        # TODO: Real output format
+        html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page'))
+        doc = html_conv(doc)
+        from array import array
+        out = array('u')
+        doc.write(out.fromunicode, namespaces={html.namespace: ''}, method='xml')
+        content = Markup(out.tounicode())
+    elif isinstance(item, NonExistent):
+        return redirect(url_for('frontend.show_item', item_name=item_name))
+    else:
+        content = u"highlighting not supported"
+    return render_template('highlight.html',
+                           item=item, item_name=item.name,
+                           data_text=content,
+                          )
+
+
+@frontend.route('/+modify/<itemname:item_name>', methods=['GET', 'POST'])
+def modify_item(item_name):
+    """Modify the wiki item item_name.
+
+    On GET, displays a form.
+    On POST, saves the new page (unless there's an error in input, or cancelled).
+    After successful POST, redirects to the page.
+    """
+    mimetype = request.values.get('mimetype')
+    template_name = request.values.get('template')
+    try:
+        item = Item.create(item_name, mimetype=mimetype)
+    except AccessDeniedError:
+        abort(403)
+    if request.method == 'GET':
+        if not flaskg.user.may.write(item_name):
+            abort(403)
+        content = item.do_modify(template_name)
+        return content
+    elif request.method == 'POST':
+        cancelled = 'button_cancel' in request.form
+        if not cancelled:
+            form = TextChaizedForm.from_flat(request.form)
+            TextCha(form).amend_form()
+            valid = form.validate()
+            if not valid:
+                data_text = request.values.get('data_text')
+                meta_text = item.meta_dict_to_text(item.meta)
+                comment = request.values.get('comment')
+                return render_template(item.template,
+                                       item_name=item_name,
+                                       gen=make_generator(),
+                                       form=form,
+                                       data_text=data_text,
+                                       meta_text=meta_text,
+                                       comment=comment,
+                                       cols=COLS,
+                                       rows_data=ROWS_DATA,
+                                       rows_meta=ROWS_META,
+                                      )
+            try:
+                item.modify()
+                item_modified.send(app._get_current_object(),
+                                   item_name=item_name)
+                if mimetype in ('application/x-twikidraw', 'application/x-anywikidraw', 'application/x-svgdraw'):
+                    # TWikiDraw/AnyWikiDraw/SvgDraw POST more than once, redirecting would break them
+                    return "OK"
+            except AccessDeniedError:
+                abort(403)
+        return redirect(url_for('frontend.show_item', item_name=item_name))
+
+
+@frontend.route('/+revert/<int:rev>/<itemname:item_name>', methods=['GET', 'POST'])
+def revert_item(item_name, rev):
+    try:
+        item = Item.create(item_name, rev_no=rev)
+    except AccessDeniedError:
+        abort(403)
+    form = TextChaizedForm.from_flat(request.form)
+    TextCha(form).amend_form()
+    if request.method == 'GET':
+        return render_template(item.revert_template, rev_no=rev,
+                               item=item, item_name=item_name,
+                               form=form,
+                               gen=make_generator(),
+                              )
+    elif request.method == 'POST':
+        if 'button_ok' in request.form:
+            valid = form.validate()
+            if not valid:
+                comment = request.values.get('comment')
+                return render_template(item.revert_template,
+                                       item=item, item_name=item_name,
+                                       rev_no=rev,
+                                       form=form,
+                                       gen=make_generator(),
+                                       comment=comment,
+                                      )
+            item.revert()
+        return redirect(url_for('frontend.show_item', item_name=item_name))
+
+
+@frontend.route('/+copy/<itemname:item_name>', methods=['GET', 'POST'])
+def copy_item(item_name):
+    try:
+        item = Item.create(item_name)
+    except AccessDeniedError:
+        abort(403)
+    if request.method == 'GET':
+        return render_template(item.copy_template,
+                               item=item, item_name=item_name,
+                              )
+    if request.method == 'POST':
+        if 'button_ok' in request.form:
+            target = request.form.get('target')
+            comment = request.form.get('comment')
+            item.copy(target, comment)
+            redirect_to = target
+        else:
+            redirect_to = item_name
+        return redirect(url_for('frontend.show_item', item_name=redirect_to))
+
+
+@frontend.route('/+rename/<itemname:item_name>', methods=['GET', 'POST'])
+def rename_item(item_name):
+    try:
+        item = Item.create(item_name)
+    except AccessDeniedError:
+        abort(403)
+    form = TextChaizedForm.from_flat(request.form)
+    TextCha(form).amend_form()
+    if request.method == 'GET':
+        return render_template(item.rename_template,
+                               item=item, item_name=item_name,
+                               form=form,
+                               gen=make_generator(),
+                              )
+    if request.method == 'POST':
+        if 'button_ok' in request.form:
+            target = request.form.get('target')
+            comment = request.form.get('comment')
+            valid = form.validate()
+            if not valid:
+                return render_template(item.rename_template,
+                                       item=item, item_name=item_name,
+                                       form=form,
+                                       gen=make_generator(),
+                                       comment=comment,
+                                      )
+            item.rename(target, comment)
+            redirect_to = target
+        else:
+            redirect_to = item_name
+        return redirect(url_for('frontend.show_item', item_name=redirect_to))
+
+
+@frontend.route('/+delete/<itemname:item_name>', methods=['GET', 'POST'])
+def delete_item(item_name):
+    try:
+        item = Item.create(item_name)
+    except AccessDeniedError:
+        abort(403)
+    form = TextChaizedForm.from_flat(request.form)
+    TextCha(form).amend_form()
+    if request.method == 'GET':
+        return render_template(item.delete_template,
+                               item=item, item_name=item_name,
+                               form=form,
+                               gen=make_generator(),
+                              )
+    elif request.method == 'POST':
+        if 'button_ok' in request.form:
+            valid = form.validate()
+            if not valid:
+                comment = request.values.get('comment')
+                return render_template(item.delete_template,
+                                       item=item, item_name=item_name,
+                                       form=form,
+                                       gen=make_generator(),
+                                       comment=comment,
+                                      )
+            comment = request.form.get('comment')
+            item.delete(comment)
+        return redirect(url_for('frontend.show_item', item_name=item_name))
+
+
+@frontend.route('/+destroy/<int:rev>/<itemname:item_name>', methods=['GET', 'POST'])
+@frontend.route('/+destroy/<itemname:item_name>', methods=['GET', 'POST'], defaults=dict(rev=None))
+def destroy_item(item_name, rev):
+    if rev is None:
+        # no revision given
+        _rev = -1 # for item creation
+        destroy_item = True
+    else:
+        _rev = rev
+        destroy_item = False
+    try:
+        item = Item.create(item_name, rev_no=_rev)
+    except AccessDeniedError:
+        abort(403)
+    form = TextChaizedForm.from_flat(request.form)
+    TextCha(form).amend_form()
+    if request.method == 'GET':
+        return render_template(item.destroy_template,
+                               item=item, item_name=item_name,
+                               rev_no=rev,
+                               form=form,
+                               gen=make_generator(),
+                              )
+    if request.method == 'POST':
+        if 'button_ok' in request.form:
+            comment = request.form.get('comment')
+            valid = form.validate()
+            if not valid:
+                return render_template(item.destroy_template,
+                                       item=item, item_name=item_name,
+                                       rev_no=rev,
+                                       form=form,
+                                       gen=make_generator(),
+                                       comment=comment,
+                                      )
+            item.destroy(comment=comment, destroy_item=destroy_item)
+        return redirect(url_for('frontend.show_item', item_name=item_name))
+
+
+@frontend.route('/+index/<itemname:item_name>')
+def index(item_name):
+    try:
+        item = Item.create(item_name)
+    except AccessDeniedError:
+        abort(403)
+    index = item.flat_index()
+    return render_template(item.index_template,
+                           item=item, item_name=item_name,
+                           index=index,
+                          )
+
+
+@frontend.route('/+index')
+def global_index():
+    item = Item.create('') # XXX hack: item_name='' gives toplevel index
+    index = item.flat_index()
+    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    return render_template('global_index.html',
+                           item_name=item_name, # XXX no item
+                           index=index,
+                          )
+
+
+@frontend.route('/+backrefs/<itemname:item_name>')
+def backrefs(item_name):
+    """
+    Returns the list of all items that link or transclude item_name
+
+    @param item_name: the name of the current item
+    @type item_name: unicode
+    @return: a page with all the items which link or transclude item_name
+    """
+    refs_here = _backrefs(flaskg.storage.iteritems(), item_name)
+    return render_template('item_link_list.html',
+                           item_name=item_name,
+                           headline=_(u'Refers Here'),
+                           item_names=refs_here
+                          )
+
+
+def _backrefs(items, item_name):
+    """
+    Returns a list with all names of items which ref item_name
+
+    @param items: all the items
+    @type items: iteratable sequence
+    @param item_name: the name of the item transcluded or linked
+    @type item_name: unicode
+    @return: the list of all items which ref item_name
+    """
+    refs_here = []
+    for item in items:
+        current_item = item.name
+        try:
+            current_revision = item.get_revision(-1)
+        except NoSuchRevisionError:
+            continue
+        links = current_revision.get(ITEMLINKS, [])
+        transclusions = current_revision.get(ITEMTRANSCLUSIONS, [])
+
+        refs = set(links + transclusions)
+        if item_name in refs:
+            refs_here.append(current_item)
+    return refs_here
+
+
+@frontend.route('/+search')
+def search():
+    return _search()
+
+
+def _search(**args):
+    return "searching for %r not implemented yet" % args
+
+
+@frontend.route('/+history/<itemname:item_name>')
+def history(item_name):
+    history = flaskg.storage.history(item_name=item_name)
+    return render_template('history.html',
+                           item_name=item_name, # XXX no item here
+                           history=history,
+                          )
+
+
+@frontend.route('/+history')
+def global_history():
+    history = flaskg.storage.history(item_name='')
+    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    return render_template('global_history.html',
+                           item_name=item_name, # XXX no item
+                           history=history,
+                          )
+
+@frontend.route('/+wanteds')
+def wanted_items():
+    """ Returns a page with the list of non-existing items, which are wanted items and the
+        items they are linked or transcluded to helps show what items still need
+        to be written and shows whether there are any broken links. """
+    wanteds = _wanteds(flaskg.storage.iteritems())
+    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    return render_template('wanteds.html',
+                           headline=_(u'Wanted Items'),
+                           item_name=item_name,
+                           wanteds=wanteds)
+
+
+def _wanteds(items):
+    """
+    Returns a dict with all the names of non-existing items which are refed by
+    other items and the items which are refed by
+
+    @param items: all the items
+    @type items: iteratable sequence
+    @return: a dict with all the wanted items and the items which are beign refed by
+    """
+    all_items = set()
+    wanteds = {}
+    for item in items:
+        current_item = item.name
+        all_items.add(current_item)
+        try:
+            current_rev = item.get_revision(-1)
+        except NoSuchRevisionError:
+            continue
+        # converting to sets so we can get the union
+        outgoing_links = current_rev.get(ITEMLINKS, [])
+        outgoing_transclusions = current_rev.get(ITEMTRANSCLUSIONS, [])
+        outgoing_refs = set(outgoing_transclusions + outgoing_links)
+        for refed_item in outgoing_refs:
+            if refed_item not in all_items:
+                if refed_item not in wanteds:
+                    wanteds[refed_item] = []
+                wanteds[refed_item].append(current_item)
+        if current_item in wanteds:
+            # if a previously wanted item has been found in the items storage, remove it
+            del wanteds[current_item]
+
+    return wanteds
+
+
+@frontend.route('/+orphans')
+def orphaned_items():
+    """ Return a page with the list of items not being linked or transcluded
+        by any other items, that makes
+        them sometimes not discoverable. """
+    orphan = _orphans(flaskg.storage.iteritems())
+    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    return render_template('item_link_list.html',
+                           item_name=item_name,
+                           headline=_(u'Orphaned Items'),
+                           item_names=orphan)
+
+
+def _orphans(items):
+    """
+    Returns a list with the names of all existing items not being refed by any other item
+
+    @param items: the list of all items
+    @type items: iteratable sequence
+    @return: the list of all orphaned items
+    """
+    linked_items = set()
+    transcluded_items = set()
+    all_items = set()
+    norev_items = set()
+    for item in items:
+        all_items.add(item.name)
+        try:
+            current_rev = item.get_revision(-1)
+        except NoSuchRevisionError:
+            norev_items.add(item.name)
+        else:
+            linked_items.update(current_rev.get(ITEMLINKS, []))
+            transcluded_items.update(current_rev.get(ITEMTRANSCLUSIONS, []))
+    orphans = all_items - linked_items - transcluded_items - norev_items
+    logging.info("_orphans: Ignored %d item(s) that have no revisions" % len(norev_items))
+    return list(orphans)
+
+
+@frontend.route('/+quicklink/<itemname:item_name>')
+def quicklink_item(item_name):
+    """ Add/Remove the current wiki page to/from the user quicklinks """
+    u = flaskg.user
+    msg = None
+    if not u.valid:
+        msg = _("You must login to use this action: %(action)s.", action="quicklink/quickunlink"), "error"
+    elif not flaskg.user.isQuickLinkedTo([item_name]):
+        if not u.addQuicklink(item_name):
+            msg = _('A quicklink to this page could not be added for you.'), "error"
+    else:
+        if not u.removeQuicklink(item_name):
+            msg = _('Your quicklink to this page could not be removed.'), "error"
+    if msg:
+        flash(*msg)
+    return redirect(url_for('frontend.show_item', item_name=item_name))
+
+
+@frontend.route('/+subscribe/<itemname:item_name>')
+def subscribe_item(item_name):
+    """ Add/Remove the current wiki item to/from the user's subscriptions """
+    u = flaskg.user
+    cfg = app.cfg
+    msg = None
+    if not u.valid:
+        msg = _("You must login to use this action: %(action)s.", action="subscribe/unsubscribe"), "error"
+    elif not u.may.read(item_name):
+        msg = _("You are not allowed to subscribe to an item you may not read."), "error"
+    elif not cfg.mail_enabled:
+        msg = _("This wiki is not enabled for mail processing."), "error"
+    elif not u.email:
+        msg = _("Add your email address in your user settings to use subscriptions."), "error"
+    elif u.isSubscribedTo([item_name]):
+        # Try to unsubscribe
+        if not u.unsubscribe(item_name):
+            msg = _("Can't remove regular expression subscription!") + u' ' + \
+                  _("Edit the subscription regular expressions in your settings."), "error"
+    else:
+        # Try to subscribe
+        if not u.subscribe(item_name):
+            msg = _('You could not get subscribed to this item.'), "error"
+    if msg:
+        flash(*msg)
+    return redirect(url_for('frontend.show_item', item_name=item_name))
+
+
+class ValidRegistration(Validator):
+    """Validator for a valid registration form
+    """
+    passwords_mismatch_msg = L_('The passwords do not match.')
+
+    def validate(self, element, state):
+        if not (element['username'].valid and
+                element['password1'].valid and element['password2'].valid and
+                element['email'].valid and element['textcha'].valid):
+            return False
+        if element['password1'].value != element['password2'].value:
+            return self.note_error(element, state, 'passwords_mismatch_msg')
+
+        return True
+
+class RegistrationForm(TextChaizedForm):
+    """a simple user registration form"""
+    name = 'register'
+
+    username = String.using(label=L_('Name')).validated_by(Present())
+    password1 = String.using(label=L_('Password')).validated_by(Present())
+    password2 = String.using(label=L_('Password')).validated_by(Present())
+    email = String.using(label=L_('E-Mail')).validated_by(IsEmail())
+    openid = String.using(label=L_('OpenID'), optional=True).validated_by(URLValidator())
+    submit = String.using(default=L_('Register'), optional=True)
+
+    validators = [ValidRegistration()]
+
+
+class OpenIDForm(TextChaizedForm):
+    """
+    OpenID registration form, inherited from the simple registration form.
+    """
+    name = 'openid'
+
+    username = String.using(label=L_('Name')).validated_by(Present())
+    password1 = String.using(label=L_('Password')).validated_by(Present())
+    password2 = String.using(label=L_('Password')).validated_by(Present())
+
+    email = String.using(label=L_('E-Mail')).validated_by(IsEmail())
+    openid = String.using(label=L_('OpenID')).validated_by(URLValidator())
+    submit = String.using(optional=True)
+
+    validators = [ValidRegistration()]
+
+def _using_moin_auth():
+    """Check if MoinAuth is being used for authentication.
+
+    Only then users can register with moin or change their password via moin.
+    """
+    from MoinMoin.auth import MoinAuth
+    for auth in app.cfg.auth:
+        if isinstance(auth, MoinAuth):
+            return True
+    return False
+
+
+def _using_openid_auth():
+    """Check if OpenIDAuth is being used for authentication.
+
+    Only then users can register with openid or change their password via openid.
+    """
+    from MoinMoin.auth.openidrp import OpenIDAuth
+    for auth in app.cfg.auth:
+        if isinstance(auth, OpenIDAuth):
+            return True
+    return False
+
+
+@frontend.route('/+register', methods=['GET', 'POST'])
+def register():
+    item_name = 'Register' # XXX
+    # is openid_submit in the form?
+    isOpenID = 'openid_submit' in request.values
+
+    if isOpenID:
+        # this is an openid continuation
+        if not _using_openid_auth():
+            return Response('No OpenIDAuth in auth list', 403)
+
+        template = 'openid_register.html'
+        if request.method == 'GET':
+            form = OpenIDForm.from_defaults()
+            # we got an openid from the multistage redirect
+            oid = request.values.get('openid_openid')
+            if oid:
+                form['openid'] = oid
+            TextCha(form).amend_form()
+
+        elif request.method == 'POST':
+            form = OpenIDForm.from_flat(request.form)
+            TextCha(form).amend_form()
+
+            valid = form.validate()
+            if valid:
+                    msg = user.create_user(username=form['username'].value,
+                                           password=form['password1'].value,
+                                           email=form['email'].value,
+                                           openid=form['openid'].value,
+                                          )
+                    if msg:
+                        flash(msg, "error")
+                    else:
+                        flash(_('Account created, please log in now.'), "info")
+                        return redirect(url_for('frontend.show_root'))
+
+    else:
+        # not openid registration and no MoinAuth
+        if not _using_moin_auth():
+            return Response('No MoinAuth in auth list', 403)
+
+        template = 'register.html'
+        if request.method == 'GET':
+            form = RegistrationForm.from_defaults()
+            TextCha(form).amend_form()
+
+        elif request.method == 'POST':
+            form = RegistrationForm.from_flat(request.form)
+            TextCha(form).amend_form()
+
+            valid = form.validate()
+            if valid:
+                msg = user.create_user(username=form['username'].value,
+                                       password=form['password1'].value,
+                                       email=form['email'].value,
+                                       openid=form['openid'].value,
+                                      )
+                if msg:
+                    flash(msg, "error")
+                else:
+                    flash(_('Account created, please log in now.'), "info")
+                    return redirect(url_for('frontend.show_root'))
+
+    return render_template(template,
+                           item_name=item_name,
+                           gen=make_generator(),
+                           form=form,
+                          )
+
+
+class ValidLostPassword(Validator):
+    """Validator for a valid lost password form
+    """
+    name_or_email_needed_msg = L_('Your user name or your email address is needed.')
+
+    def validate(self, element, state):
+        if not(element['username'].valid and element['username'].value
+               or
+               element['email'].valid and element['email'].value):
+            return self.note_error(element, state, 'name_or_email_needed_msg')
+
+        return True
+
+
+class PasswordLostForm(Form):
+    """a simple password lost form"""
+    name = 'lostpass'
+
+    username = String.using(label=L_('Name'), optional=True)
+    email = String.using(label=L_('E-Mail'), optional=True).validated_by(IsEmail())
+    submit = String.using(default=L_('Recover password'), optional=True)
+
+    validators = [ValidLostPassword()]
+
+
+@frontend.route('/+lostpass', methods=['GET', 'POST'])
+def lostpass():
+    # TODO use ?next=next_location check if target is in the wiki and not outside domain
+    item_name = 'LostPass' # XXX
+
+    if not _using_moin_auth():
+        return Response('No MoinAuth in auth list', 403)
+
+    if request.method == 'GET':
+        form = PasswordLostForm.from_defaults()
+        return render_template('lostpass.html',
+                               item_name=item_name,
+                               gen=make_generator(),
+                               form=form,
+                              )
+    if request.method == 'POST':
+        form = PasswordLostForm.from_flat(request.form)
+        valid = form.validate()
+        if valid:
+            u = None
+            username = form['username'].value
+            if username:
+                u = user.User(user.getUserId(username))
+            email = form['email'].value
+            if form['email'].valid and email:
+                u = user.get_by_email_address(email)
+            if u and u.valid:
+                is_ok, msg = u.mailAccountData()
+                if not is_ok:
+                    flash(msg, "error")
+            flash(_("If this account exists, you will be notified."), "info")
+            return redirect(url_for('frontend.show_root'))
+        else:
+            return render_template('lostpass.html',
+                                   item_name=item_name,
+                                   gen=make_generator(),
+                                   form=form,
+                                  )
+
+class ValidPasswordRecovery(Validator):
+    """Validator for a valid password recovery form
+    """
+    passwords_mismatch_msg = L_('The passwords do not match.')
+    password_encoding_problem_msg = L_('New password is unacceptable, encoding trouble.')
+
+    def validate(self, element, state):
+        if element['password1'].value != element['password2'].value:
+            return self.note_error(element, state, 'passwords_mismatch_msg')
+
+        try:
+            user.encodePassword(element['password1'].value)
+        except UnicodeError:
+            return self.note_error(element, state, 'password_encoding_problem_msg')
+
+        return True
+
+class PasswordRecoveryForm(Form):
+    """a simple password recovery form"""
+    name = 'recoverpass'
+
+    username = String.using(label=L_('Name')).validated_by(Present())
+    token = String.using(label=L_('Recovery token')).validated_by(Present())
+    password1 = String.using(label=L_('New password')).validated_by(Present())
+    password2 = String.using(label=L_('New password (repeat)')).validated_by(Present())
+    submit = String.using(default=L_('Change password'), optional=True)
+
+    validators = [ValidPasswordRecovery()]
+
+
+@frontend.route('/+recoverpass', methods=['GET', 'POST'])
+def recoverpass():
+    # TODO use ?next=next_location check if target is in the wiki and not outside domain
+    item_name = 'RecoverPass' # XXX
+
+    if not _using_moin_auth():
+        return Response('No MoinAuth in auth list', 403)
+
+    if request.method == 'GET':
+        form = PasswordRecoveryForm.from_defaults()
+        form.update(request.values)
+        return render_template('recoverpass.html',
+                               item_name=item_name,
+                               gen=make_generator(),
+                               form=form,
+                              )
+    if request.method == 'POST':
+        form = PasswordRecoveryForm.from_flat(request.form)
+        valid = form.validate()
+        if valid:
+            u = user.User(user.getUserId(form['username'].value))
+            if u and u.valid and u.apply_recovery_token(form['token'].value, form['password1'].value):
+                flash(_("Your password has been changed, you can log in now."), "info")
+            else:
+                flash(_('Your token is invalid!'), "error")
+            return redirect(url_for('frontend.show_root'))
+        else:
+            return render_template('recoverpass.html',
+                                   item_name=item_name,
+                                   gen=make_generator(),
+                                   form=form,
+                                  )
+
+
+class ValidLogin(Validator):
+    """
+    Login validator
+    """
+    moin_fail_msg = L_('Either your username or password was invalid.')
+    openid_fail_msg = L_('Failed to authenticate with this OpenID.')
+
+    def validate(self, element, state):
+        # get the result from the other validators
+        moin_valid = element['username'].valid and element['password'].valid
+        openid_valid = element['openid'].valid
+
+        # none of them was valid
+        if not (openid_valid or moin_valid):
+            return False
+        # got our user!
+        if flaskg.user.valid:
+            return True
+        # no valid user -> show appropriate message
+        else:
+            if not openid_valid:
+                return self.note_error(element, state, 'openid_fail_msg')
+            elif not moin_valid:
+                return self.note_error(element, state, 'moin_fail_msg')
+
+
+class LoginForm(Form):
+    """
+    Login form
+    """
+    name = 'login'
+
+    username = String.using(label=L_('Name'), optional=True).validated_by(Present())
+    password = String.using(label=L_('Password'), optional=True).validated_by(Present())
+    openid = String.using(label=L_('OpenID'), optional=True).validated_by(Present(), URLValidator())
+
+    # the submit hidden field
+    submit = String.using(optional=True)
+
+    validators = [ValidLogin()]
+
+
+@frontend.route('/+login', methods=['GET', 'POST'])
+def login():
+    # TODO use ?next=next_location check if target is in the wiki and not outside domain
+    item_name = 'Login' # XXX
+
+    # multistage return
+    if flaskg._login_multistage_name == 'openid':
+            return Response(flaskg._login_multistage, mimetype='text/html')
+
+    # get the form contents
+    form = LoginForm.from_flat(request.form)
+    valid = form.validate()
+    if valid:
+        # we have a logged-in, valid user
+        return redirect(url_for('frontend.show_root'))
+
+    # flash the error messages (if any)
+    for msg in flaskg._login_messages:
+            flash(msg, "error")
+
+    if request.method == 'GET':
+        form = LoginForm.from_defaults()
+        for authmethod in app.cfg.auth:
+            hint = authmethod.login_hint()
+            if hint:
+                flash(hint, "info")
+
+        # initialise form
+        form.set_default()
+        return render_template('login.html',
+                               item_name=item_name,
+                               login_inputs=app.cfg.auth_login_inputs,
+                               gen=make_generator(),
+                               form=form,
+                              )
+    if request.method == 'POST':
+        # if no valid user, show form again (with hints)
+        return render_template('login.html',
+                               item_name=item_name,
+                               login_inputs=app.cfg.auth_login_inputs,
+                               gen=make_generator(),
+                               form=form,
+                              )
+
+
+@frontend.route('/+logout')
+def logout():
+    flash(_("You are now logged out."), "info")
+    for key in ['user.id', 'user.auth_method', 'user.auth_attribs', ]:
+        if key in session:
+            del session[key]
+    return redirect(url_for('frontend.show_root'))
+
+
+class ValidChangePass(Validator):
+    """Validator for a valid password change
+    """
+    passwords_mismatch_msg = L_('The passwords do not match.')
+    current_password_wrong_msg = L_('The current password was wrong.')
+    password_encoding_problem_msg = L_('New password is unacceptable, encoding trouble.')
+
+    def validate(self, element, state):
+        if not (element['password_current'].valid and element['password1'].valid and element['password2'].valid):
+            return False
+
+        if not user.User(name=flaskg.user.name, password=element['password_current'].value).valid:
+            return self.note_error(element, state, 'current_password_wrong_msg')
+
+        if element['password1'].value != element['password2'].value:
+            return self.note_error(element, state, 'passwords_mismatch_msg')
+
+        try:
+            user.encodePassword(element['password1'].value)
+        except UnicodeError:
+            return self.note_error(element, state, 'password_encoding_problem_msg')
+        return True
+
+
+class UserSettingsPasswordForm(Form):
+    name = 'usersettings_password'
+    password_current = String.using(label=L_('Current Password')).validated_by(Present())
+    password1 = String.using(label=L_('New password')).validated_by(Present())
+    password2 = String.using(label=L_('New password (repeat)')).validated_by(Present())
+    submit = String.using(default=L_('Change password'), optional=True)
+    validators = [ValidChangePass()]
+
+
+class UserSettingsNotificationForm(Form):
+    name = 'usersettings_notification'
+    email = String.using(label=L_('E-Mail')).validated_by(IsEmail())
+    submit = String.using(default=L_('Save'), optional=True)
+
+
+class UserSettingsNavigationForm(Form):
+    name = 'usersettings_navigation'
+    # TODO: find a good way to handle quicklinks here
+    submit = String.using(default=L_('Save'), optional=True)
+
+
+class UserSettingsOptionsForm(Form):
+    # TODO: if the checkbox in the form is checked, we get key: u'1' in the
+    # form data and all is fine. if it is not checked, the key is not present
+    # in the form data and flatland assigns None to the attribute (not False).
+    # If moin detects the None, it thinks this has not been set and uses its
+    # builtin defaults (for some True, for some others False). Makes
+    # edit_on_doubleclick malfunctioning (because its default is True).
+    name = 'usersettings_options'
+    mailto_author = Boolean.using(label=L_('Publish my email (not my wiki homepage) in author info'), optional=True)
+    edit_on_doubleclick = Boolean.using(label=L_('Open editor on double click'), optional=True)
+    show_comments = Boolean.using(label=L_('Show comment sections'), optional=True)
+    disabled = Boolean.using(label=L_('Disable this account forever'), optional=True)
+    submit = String.using(default=L_('Save'), optional=True)
+
+
+@frontend.route('/+usersettings', defaults=dict(part='main'), methods=['GET'])
+@frontend.route('/+usersettings/<part>', methods=['GET', 'POST'])
+def usersettings(part):
+    # TODO use ?next=next_location check if target is in the wiki and not outside domain
+    item_name = 'User Settings' # XXX
+
+    # these forms can't be global because we need app object, which is only available within a request:
+    class UserSettingsPersonalForm(Form):
+        name = 'usersettings_personal' # "name" is duplicate
+        name = String.using(label=L_('Name')).validated_by(Present())
+        aliasname = String.using(label=L_('Alias-Name'), optional=True)
+        openid = String.using(label=L_('OpenID'), optional=True).validated_by(URLValidator())
+        #timezones_keys = sorted(Locale('en').time_zones.keys())
+        timezones_keys = [unicode(tz) for tz in pytz.common_timezones]
+        timezone = Enum.using(label=L_('Timezone')).valued(*timezones_keys)
+        supported_locales = [Locale('en')] + app.babel_instance.list_translations()
+        locales_available = sorted([(unicode(l), l.display_name) for l in supported_locales],
+                                   key=lambda x: x[1])
+        locales_keys = [l[0] for l in locales_available]
+        locale = Enum.using(label=L_('Locale')).with_properties(labels=dict(locales_available)).valued(*locales_keys)
+        submit = String.using(default=L_('Save'), optional=True)
+
+    class UserSettingsUIForm(Form):
+        name = 'usersettings_ui'
+        themes_available = sorted([(unicode(t.identifier), t.name) for t in get_themes_list()],
+                                  key=lambda x: x[1])
+        themes_keys = [t[0] for t in themes_available]
+        theme_name = Enum.using(label=L_('Theme name')).with_properties(labels=dict(themes_available)).valued(*themes_keys)
+        css_url = String.using(label=L_('User CSS URL'), optional=True).validated_by(URLValidator())
+        edit_rows = Integer.using(label=L_('Editor size')).validated_by(Converted())
+        submit = String.using(default=L_('Save'), optional=True)
+
+    dispatch = dict(
+        personal=UserSettingsPersonalForm,
+        password=UserSettingsPasswordForm,
+        notification=UserSettingsNotificationForm,
+        ui=UserSettingsUIForm,
+        navigation=UserSettingsNavigationForm,
+        options=UserSettingsOptionsForm,
+    )
+    FormClass = dispatch.get(part)
+    if FormClass is None:
+        # 'main' part or some invalid part
+        return render_template('usersettings.html',
+                               part='main',
+                               item_name=item_name,
+                              )
+    if request.method == 'GET':
+        form = FormClass.from_object(flaskg.user)
+        form['submit'].set('Save') # XXX why does from_object() kill submit value?
+        return render_template('usersettings.html',
+                               item_name=item_name,
+                               part=part,
+                               gen=make_generator(),
+                               form=form,
+                              )
+    if request.method == 'POST':
+        form = FormClass.from_flat(request.form)
+        valid = form.validate()
+        if valid:
+            # successfully modified everything
+            success = True
+            if part == 'password':
+                flaskg.user.enc_password = user.encodePassword(form['password1'].value)
+                flash(_("Your password has been changed."), "info")
+            else:
+                if part == 'personal':
+                    if form['openid'].value != flaskg.user.openid and user.get_by_openid(form['openid'].value):
+                        # duplicate openid
+                        flash(_("This openid is already in use."), "error")
+                        success = False
+                    if form['name'].value != flaskg.user.name and user.getUserId(form['name'].value):
+                        # duplicate name
+                        flash(_("This username is already in use."), "error")
+                        success = False
+                if part == 'notification':
+                    if (form['email'].value != flaskg.user.email and
+                        user.get_by_email_address(form['email'].value) and app.cfg.user_email_unique):
+                        # duplicate email
+                        flash(_('This email is already in use'), 'error')
+                        success = False
+                if success:
+                    form.update_object(flaskg.user)
+                    flaskg.user.save()
+                    return redirect(url_for('frontend.usersettings'))
+                else:
+                    # reset to valid values
+                    form = FormClass.from_object(flaskg.user)
+
+        return render_template('usersettings.html',
+                               item_name=item_name,
+                               part=part,
+                               gen=make_generator(),
+                               form=form,
+                              )
+
+
+@frontend.route('/+bookmark')
+def bookmark():
+    """ set bookmark (in time) for recent changes (or delete them) """
+    if flaskg.user.valid:
+        timestamp = request.values.get('time')
+        if timestamp is not None:
+            if timestamp == 'del':
+                tm = None
+            else:
+                try:
+                    tm = int(timestamp)
+                except StandardError:
+                    tm = int(time.time())
+        else:
+            tm = int(time.time())
+
+        if tm is None:
+            flaskg.user.delBookmark()
+        else:
+            flaskg.user.setBookmark(tm)
+    else:
+        flash(_("You must log in to use bookmarks."), "error")
+    return redirect(url_for('frontend.global_history'))
+
+
+@frontend.route('/+diffraw/<path:item_name>')
+def diffraw(item_name):
+    # TODO get_item and get_revision calls may raise an AccessDeniedError.
+    #      If this happens for get_item, don't show the diff at all
+    #      If it happens for get_revision, we may just want to skip that rev in the list
+    try:
+        item = flaskg.storage.get_item(item_name)
+    except AccessDeniedError:
+        abort(403)
+    rev1 = request.values.get('rev1')
+    rev2 = request.values.get('rev2')
+    return _diff_raw(item, rev1, rev2)
+
+
+@frontend.route('/+diffsince/<int:timestamp>/<path:item_name>')
+def diffsince(item_name, timestamp):
+    date = timestamp
+    # this is how we get called from "recent changes"
+    # try to find the latest rev1 before bookmark <date>
+    try:
+        item = flaskg.storage.get_item(item_name)
+    except AccessDeniedError:
+        abort(403)
+    revnos = item.list_revisions()
+    revnos.reverse()  # begin with latest rev
+    for revno in revnos:
+        revision = item.get_revision(revno)
+        if revision.timestamp <= date:
+            rev1 = revision.revno
+            break
+    else:
+        rev1 = revno  # if we didn't find a rev, we just take oldest rev we have
+    rev2 = -1  # and compare it with latest we have
+    return _diff(item, rev1, rev2)
+
+
+@frontend.route('/+diff/<path:item_name>')
+def diff(item_name):
+    # TODO get_item and get_revision calls may raise an AccessDeniedError.
+    #      If this happens for get_item, don't show the diff at all
+    #      If it happens for get_revision, we may just want to skip that rev in the list
+    try:
+        item = flaskg.storage.get_item(item_name)
+    except AccessDeniedError:
+        abort(403)
+    rev1 = request.values.get('rev1')
+    rev2 = request.values.get('rev2')
+    return _diff(item, rev1, rev2)
+
+
+def _normalize_revnos(item, revno1, revno2):
+    try:
+        revno1 = int(revno1)
+    except (ValueError, TypeError):
+        revno1 = -2
+    try:
+        revno2 = int(revno2)
+    except (ValueError, TypeError):
+        revno2 = -1
+
+    # get (absolute) current revision number
+    current_revno = item.get_revision(-1).revno
+    # now we can calculate the absolute revnos if we don't have them yet
+    if revno1 < 0:
+        revno1 += current_revno + 1
+    if revno2 < 0:
+        revno2 += current_revno + 1
+
+    if revno1 > revno2:
+        oldrevno, newrevno = revno2, revno1
+    else:
+        oldrevno, newrevno = revno1, revno2
+    return oldrevno, newrevno
+
+
+def _common_mimetype(rev1, rev2):
+    mt1 = rev1.get(MIMETYPE)
+    mt2 = rev2.get(MIMETYPE)
+    if mt1 == mt2:
+        # easy, exactly the same mimetype, call do_diff for it
+        commonmt = mt1
+    else:
+        major1 = mt1.split('/')[0]
+        major2 = mt2.split('/')[0]
+        if major1 == major2:
+            # at least same major mimetype, use common base item class
+            commonmt = major1 + '/'
+        else:
+            # nothing in common
+            commonmt = ''
+    return commonmt
+
+
+def _diff(item, revno1, revno2):
+    oldrevno, newrevno = _normalize_revnos(item, revno1, revno2)
+    oldrev = item.get_revision(oldrevno)
+    newrev = item.get_revision(newrevno)
+
+    commonmt = _common_mimetype(oldrev, newrev)
+
+    try:
+        item = Item.create(item.name, mimetype=commonmt, rev_no=newrevno)
+    except AccessDeniedError:
+        abort(403)
+    rev_nos = item.rev.item.list_revisions()
+    return render_template(item.diff_template,
+                           item=item, item_name=item.name,
+                           rev=item.rev,
+                           first_rev_no=rev_nos[0],
+                           last_rev_no=rev_nos[-1],
+                           oldrev=oldrev,
+                           newrev=newrev,
+                          )
+
+
+def _diff_raw(item, revno1, revno2):
+    oldrevno, newrevno = _normalize_revnos(item, revno1, revno2)
+    oldrev = item.get_revision(oldrevno)
+    newrev = item.get_revision(newrevno)
+
+    commonmt = _common_mimetype(oldrev, newrev)
+
+    try:
+        item = Item.create(item.name, mimetype=commonmt, rev_no=newrevno)
+    except AccessDeniedError:
+        abort(403)
+    return item._render_data_diff_raw(oldrev, newrev)
+
+
+@frontend.route('/+similar_names/<itemname:item_name>')
+def similar_names(item_name):
+    """
+    list similar item names
+
+    @copyright: 2001 Richard Jones <richard@bizarsoftware.com.au>,
+                2001 Juergen Hermann <jh@web.de>
+    @license: GNU GPL, see COPYING for details.
+    """
+    start, end, matches = findMatches(item_name)
+    keys = matches.keys()
+    keys.sort()
+    # TODO later we could add titles for the misc ranks:
+    # 8 item_name
+    # 4 "%s/..." % item_name
+    # 3 "%s...%s" % (start, end)
+    # 1 "%s..." % (start, )
+    # 2 "...%s" % (end, )
+    item_names = []
+    for wanted_rank in [8, 4, 3, 1, 2, ]:
+        for name in keys:
+            rank = matches[name]
+            if rank == wanted_rank:
+                item_names.append(name)
+    return render_template("item_link_list.html",
+                           headline=_("Items with similar names"),
+                           item_name=item_name, # XXX no item
+                           item_names=item_names)
+
+
+def findMatches(item_name, s_re=None, e_re=None):
+    """ Find similar item names.
+
+    @param item_name: name to match
+    @param s_re: start re for wiki matching
+    @param e_re: end re for wiki matching
+    @rtype: tuple
+    @return: start word, end word, matches dict
+    """
+    item_names = [item.name for item in flaskg.storage.iteritems()]
+    if item_name in item_names:
+        item_names.remove(item_name)
+    # Get matches using wiki way, start and end of word
+    start, end, matches = wikiMatches(item_name, item_names, start_re=s_re, end_re=e_re)
+    # Get the best 10 close matches
+    close_matches = {}
+    found = 0
+    for name in closeMatches(item_name, item_names):
+        if name not in matches:
+            # Skip names already in matches
+            close_matches[name] = 8
+            found += 1
+            # Stop after 10 matches
+            if found == 10:
+                break
+    # Finally, merge both dicts
+    matches.update(close_matches)
+    return start, end, matches
+
+
+def wikiMatches(item_name, item_names, start_re=None, end_re=None):
+    """
+    Get item names that starts or ends with same word as this item name.
+
+    Matches are ranked like this:
+        4 - item is subitem of item_name
+        3 - match both start and end
+        2 - match end
+        1 - match start
+
+    @param item_name: item name to match
+    @param item_names: list of item names
+    @param start_re: start word re (compile regex)
+    @param end_re: end word re (compile regex)
+    @rtype: tuple
+    @return: start, end, matches dict
+    """
+    if start_re is None:
+        start_re = re.compile('([%s][%s]+)' % (config.chars_upper,
+                                               config.chars_lower))
+    if end_re is None:
+        end_re = re.compile('([%s][%s]+)$' % (config.chars_upper,
+                                              config.chars_lower))
+
+    # If we don't get results with wiki words matching, fall back to
+    # simple first word and last word, using spaces.
+    words = item_name.split()
+    match = start_re.match(item_name)
+    if match:
+        start = match.group(1)
+    else:
+        start = words[0]
+
+    match = end_re.search(item_name)
+    if match:
+        end = match.group(1)
+    else:
+        end = words[-1]
+
+    matches = {}
+    subitem = item_name + '/'
+
+    # Find any matching item names and rank by type of match
+    for name in item_names:
+        if name.startswith(subitem):
+            matches[name] = 4
+        else:
+            if name.startswith(start):
+                matches[name] = 1
+            if name.endswith(end):
+                matches[name] = matches.get(name, 0) + 2
+
+    return start, end, matches
+
+
+def closeMatches(item_name, item_names):
+    """ Get close matches.
+
+    Return all matching item names with rank above cutoff value.
+
+    @param item_name: item name to match
+    @param item_names: list of item names
+    @rtype: list
+    @return: list of matching item names, sorted by rank
+    """
+    # Match using case insensitive matching
+    # Make mapping from lower item names to item names.
+    lower = {}
+    for name in item_names:
+        key = name.lower()
+        if key in lower:
+            lower[key].append(name)
+        else:
+            lower[key] = [name]
+
+    # Get all close matches
+    all_matches = difflib.get_close_matches(item_name.lower(), lower.keys(),
+                                            len(lower), cutoff=0.6)
+
+    # Replace lower names with original names
+    matches = []
+    for name in all_matches:
+        matches.extend(lower[name])
+
+    return matches
+
+
+@frontend.route('/+sitemap/<item_name>')
+def sitemap(item_name):
+    """
+    sitemap view shows item link structure, relative to current item
+    """
+    sitemap = NestedItemListBuilder().recurse_build([item_name])
+    del sitemap[0] # don't show current item name as sole toplevel list item
+    return render_template('sitemap.html',
+                           item_name=item_name, # XXX no item
+                           sitemap=sitemap,
+                          )
+
+
+class NestedItemListBuilder(object):
+    def __init__(self):
+        self.children = set()
+        self.numnodes = 0
+        self.maxnodes = 35 # approx. max count of nodes, not strict
+
+    def recurse_build(self, names):
+        result = []
+        if self.numnodes < self.maxnodes:
+            for name in names:
+                self.children.add(name)
+                result.append(name)
+                self.numnodes += 1
+                childs = self.childs(name)
+                if childs:
+                    childs = self.recurse_build(childs)
+                    result.append(childs)
+        return result
+
+    def childs(self, name):
+        # does not recurse
+        try:
+            item = flaskg.storage.get_item(name)
+        except AccessDeniedError:
+            return []
+        rev = item.get_revision(-1)
+        itemlinks = rev.get(ITEMLINKS, [])
+        return [child for child in itemlinks if self.is_ok(child)]
+
+    def is_ok(self, child):
+        if child not in self.children:
+            if not flaskg.user.may.read(child):
+                return False
+            if flaskg.storage.has_item(child):
+                self.children.add(child)
+                return True
+        return False
+
+
+@frontend.route('/+tags')
+def global_tags():
+    """
+    show a list or tag cloud of all tags in this wiki
+    """
+    counts_tags_names = flaskg.storage.all_tags()
+    item_name = request.values.get('item_name', '') # actions menu puts it into qs
+    if counts_tags_names:
+        # sort by tag name
+        counts_tags_names = sorted(counts_tags_names, key=lambda e: e[1])
+        # this is a simple linear scaling
+        counts = [e[0] for e in counts_tags_names]
+        count_min = min(counts)
+        count_max = max(counts)
+        weight_max = 9.99
+        if count_min == count_max:
+            scale = weight_max / 2
+        else:
+            scale = weight_max / (count_max - count_min)
+        def cls(count, tag):
+            # return the css class for this tag
+            weight = scale * (count - count_min)
+            return "weight%d" % int(weight)  # weight0, ..., weight9
+        tags = [(cls(count, tag), tag) for count, tag, names in counts_tags_names]
+    else:
+        tags = []
+    return render_template("global_tags.html",
+                           headline=_("All tags in this wiki"),
+                           item_name=item_name,
+                           tags=tags)
+
+
+@frontend.route('/+tags/<itemname:tag>')
+def tagged_items(tag):
+    """
+    show all items' names that have tag <tag>
+    """
+    item_names = flaskg.storage.tagged_items(tag)
+    return render_template("item_link_list.html",
+                           headline=_("Items tagged with %(tag)s", tag=tag),
+                           item_name=tag,
+                           item_names=item_names)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/misc/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,14 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - misc. views package
+
+    This package contains misc. stuff that doesn't fit into another view category.
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from flask import Module
+misc = Module(__name__)
+import MoinMoin.apps.misc.views
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/misc/_tests/test_misc.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - basic tests for misc views
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+class TestMisc(object):
+    def test_global_sitemap(self):
+        with self.app.test_client() as c:
+            rv = c.get('/+misc/sitemap')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'text/xml; charset=utf-8'
+            assert rv.data.startswith('<?xml')
+            assert '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' in rv.data
+            assert '</urlset>' in rv.data
+
+    def test_urls_names(self):
+        with self.app.test_client() as c:
+            rv = c.get('/+misc/urls_names')
+            assert rv.status == '200 OK'
+            assert rv.headers['Content-Type'] == 'text/plain; charset=utf-8'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/misc/templates/sitemap.xml	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+{% for item_name, lastmod, changefreq, priority in sitemap -%}
+<url>
+<loc>{{ url_for('frontend.show_item', item_name=item_name, _external=True)|e }}</loc>
+<lastmod>{{ lastmod }}</lastmod>
+<changefreq>{{ changefreq }}</changefreq>
+<priority>{{ priority }}</priority>
+</url>
+{%- endfor %}
+</urlset>
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/misc/templates/urls_names.txt	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,3 @@
+{% for item_name in item_names -%}
+{{ url_for('frontend.show_item', item_name=item_name, _external=True) }} {{ item_name }}
+{% endfor %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/misc/views.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - miscellaneous views
+
+    Misc. stuff that doesn't fit into another view category.
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+@license: GNU GPL, see COPYING for details.
+"""
+
+import time
+
+from flask import Response
+from flask import flaskg
+
+from flask import current_app as app
+
+from MoinMoin.apps.misc import misc
+
+from MoinMoin.themes import render_template
+from MoinMoin import wikiutil
+from MoinMoin.storage.error import NoSuchRevisionError, NoSuchItemError
+
+SITEMAP_HAS_SYSTEM_ITEMS = True
+
+@misc.route('/sitemap')
+def sitemap():
+    """
+    Google (and others) XML sitemap
+    """
+    def format_timestamp(ts):
+        return time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(ts))
+
+    sitemap = []
+    for item in flaskg.storage.iteritems():
+        try:
+            rev = item.get_revision(-1)
+        except NoSuchRevisionError:
+            # XXX we currently also get user items, they have no revisions -
+            # but in the end, they should not be readable by the user anyways
+            continue
+        if wikiutil.isSystemItem(item.name):
+            if not SITEMAP_HAS_SYSTEM_ITEMS:
+                continue
+            # system items are rather boring
+            changefreq = "yearly"
+            priority = "0.1"
+        else:
+            # these are the content items:
+            changefreq = "daily"
+            priority = "0.5"
+        sitemap.append((item.name, format_timestamp(rev.timestamp), changefreq, priority))
+    # add an entry for root url
+    try:
+        item = flaskg.storage.get_item(app.cfg.item_root)
+        rev = item.get_revision(-1)
+        sitemap.append((u'', format_timestamp(rev.timestamp), "hourly", "1.0"))
+    except NoSuchItemError:
+        pass
+    sitemap.sort()
+    content = render_template('misc/sitemap.xml', sitemap=sitemap)
+    return Response(content, mimetype='text/xml')
+
+
+@misc.route('/urls_names')
+def urls_names():
+    """
+    List of all item URLs and names, e.g. for sisteritems.
+
+    This view generates a list of item URLs and item names, so that other wikis
+    can implement SisterWiki functionality easily.
+    See: http://usemod.com/cgi-bin/mb.pl?SisterSitesImplementationGuide
+    """
+    # XXX we currently also get user items, fix this
+    item_names = [item.name for item in flaskg.storage.iteritems()]
+    item_names.sort()
+    content = render_template('misc/urls_names.txt', item_names=item_names)
+    return Response(content, mimetype='text/plain')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/serve/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,16 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - serve (external) static files
+
+    E.g. javascript based drawing or html editors.
+    We want to avoid bundling them, thus we access them somewhere on the
+    filesystem outside of moin.
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from flask import Module
+serve = Module(__name__)
+import MoinMoin.apps.serve.views
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/serve/views.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,39 @@
+# -*- coding: ascii -*-
+"""
+    MoinMoin - external static file serving
+
+    @copyright: 2010 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from flask import Response, abort
+from flask import send_from_directory
+
+from flask import current_app as app
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin.apps.serve import serve
+
+
+@serve.route('/')
+def index():
+    # show what we have (but not where in the filesystem)
+    content = "\n".join(app.cfg.serve_files.keys())
+    return Response(content, content_type='text/plain')
+
+
+@serve.route('/<name>/', defaults=dict(filename=''))
+@serve.route('/<name>/<path:filename>')
+def files(name, filename):
+    try:
+        base_path = app.cfg.serve_files[name]
+    except KeyError:
+        abort(404)
+
+    if not filename:
+        abort(404)
+
+    return send_from_directory(base_path, filename)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,441 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - modular authentication handling
+
+    Each authentication method is an object instance containing
+    four methods:
+      * login(user_obj, **kw)
+      * logout(user_obj, **kw)
+      * request(user_obj, **kw)
+      * login_hint()
+
+    The kw arguments that are passed in are currently:
+       attended: boolean indicating whether a user (attended=True) or
+                 a machine is requesting login, multistage auth is not
+                 currently possible for machine logins [login only]
+       username: the value of the 'username' form field (or None)
+                 [login only]
+       password: the value of the 'password' form field (or None)
+                 [login only]
+       cookie: a Cookie.SimpleCookie instance containing the cookie
+               that the browser sent
+       multistage: boolean indicating multistage login continuation
+                   [may not be present, login only]
+
+    login_hint() should return a HTML text that is displayed to the user right
+    below the login form, it should tell the user what to do in case of a
+    forgotten password and how to create an account (if applicable.)
+
+    More may be added.
+
+    The request method is called for each request except login/logout.
+
+    The 'request' and 'logout' methods must return a tuple (user_obj, continue)
+    where 'user_obj' can be
+      * None, to throw away any previous user_obj from previous auth methods
+      * the passed in user_obj for no changes
+      * a newly created MoinMoin.user.User instance
+    and 'continue' is a boolean to indicate whether the next authentication
+    method should be tried.
+
+    The 'login' method must return an instance of MoinMoin.auth.LoginReturn
+    which contains the members
+      * user_obj
+      * continue_flag
+      * multistage
+      * message
+      * redirect_to
+
+    There are some helpful subclasses derived from this class for the most
+    common cases, namely ContinueLogin(), CancelLogin(), MultistageFormLogin()
+    and MultistageRedirectLogin().
+
+    The user_obj and continue_flag members have the same semantics as for the
+    request and logout methods.
+
+    The messages that are returned by the various auth methods will be
+    displayed to the user, since they will all be displayed usually auth
+    methods will use the message feature only along with returning False for
+    the continue flag.
+
+    Note, however, that when no username is entered or the username is not
+    found in the database, it may be appropriate to return with a message
+    and the continue flag set to true (ContinueLogin) because a subsequent auth
+    plugin might work even without the username (e.g. an openid auth plugin).
+
+    The multistage member must evaluate to false or be callable. If it is
+    callable, this indicates that the authentication method requires a second
+    login stage. In that case, the multistage item will be called and should
+    return an instance of
+    MoinMoin.widget.html.FORM and the generic code will append some required
+    hidden fields to it. It is also permissible to return some valid HTML,
+    but that feature has very limited use since it breaks the authentication
+    method chain.
+
+    Note that because multistage login does not depend on anonymous session
+    support, it is possible that users jump directly into the second stage
+    by giving the appropriate parameters to the login action. Hence, auth
+    methods should take care to recheck everything and not assume the user
+    has gone through all previous stages.
+
+    If the multistage login requires querying an external site that involves
+    a redirect, the redirect_to member may be set instead of the multistage
+    member. If this is set it must be a URL that user should be redirected to.
+    Since the user must be able to come back to the authentication, any
+    "%return" in the URL is replaced with the url-encoded form of the URL
+    to the next authentication stage, any "%return_form" is replaced with
+    the url-plus-encoded form (spaces encoded as +) of the same URL.
+
+    After the user has submitted the required form or has been redirected back
+    from the external site, execution of the auth login methods resumes with
+    the auth item that requested the multistage login and its login method is
+    called with the 'multistage' keyword parameter set to True.
+
+    Each authentication method instance must also contain the members
+     * login_inputs: a list of required inputs, currently supported are
+                      - 'username': username entry field
+                      - 'password': password entry field
+                      - 'special_no_input': manual login is required
+                            but no form fields need to be filled in
+                            (e.g. openid with forced provider)
+                            in this case the theme may provide a short-
+                            cut omitting the login form
+     * logout_possible: boolean indicating whether this auth methods
+                        supports logging out
+     * name: name of the auth method, must be the same as given as the
+             user object's auth_method keyword parameter.
+
+    To simplify creating new authentication methods you can inherit from
+    MoinMoin.auth.BaseAuth that does nothing for all three methods, but
+    allows you to override only some methods.
+
+    cfg.auth is a list of authentication object instances whose methods
+    are called in the order they are listed. The session method is called
+    for every request, when logging in or out these are called before the
+    session method.
+
+    When creating a new MoinMoin.user.User object, you can give a keyword
+    argument "auth_attribs" to User.__init__ containing a list of user
+    attributes that are determined and fixed by this auth method and may
+    not be changed by the user in their preferences.
+    You also have to give the keyword argument "auth_method" containing the
+    name of the authentication method.
+
+    @copyright: 2005-2006 Bastian Blank, Florian Festi,
+                          MoinMoin:AlexanderSchremmer, Nick Phillips,
+                          MoinMoin:FrankieChow, MoinMoin:NirSoffer,
+                2005-2009 MoinMoin:ThomasWaldmann,
+                2007      MoinMoin:JohannesBerg
+
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from werkzeug import redirect, abort, url_quote, url_quote_plus
+from flask import url_for, session, request
+from flask import flaskg
+from flask import current_app as app
+from jinja2 import Markup
+
+from MoinMoin import user, wikiutil
+from MoinMoin.i18n import _, L_, N_
+
+
+def get_multistage_continuation_url(auth_name, extra_fields={}):
+    """get_continuation_url - return a multistage continuation URL
+
+       This function returns a URL that when loaded continues a multistage
+       authentication at the auth method requesting it (parameter auth_name.)
+       Additional fields are added to the URL from the extra_fields dict.
+
+       @param auth_name: name of the auth method requesting the continuation
+       @param extra_fields: extra GET fields to add to the URL
+    """
+    # logically, this belongs to request, but semantically it should
+    # live in auth so people do auth.get_multistage_continuation_url()
+
+    # the url should be absolute so we use _external
+    url = url_for('frontend.login', login_submit='1', stage=auth_name, _external=True, **extra_fields)
+    logging.debug("multistage_continuation_url: %s" % url)
+    return url
+
+
+class LoginReturn(object):
+    """ LoginReturn - base class for auth method login() return value"""
+    def __init__(self, user_obj, continue_flag, message=None, multistage=None,
+                 redirect_to=None):
+        self.user_obj = user_obj
+        self.continue_flag = continue_flag
+        self.message = message
+        self.multistage = multistage
+        self.redirect_to = redirect_to
+
+class ContinueLogin(LoginReturn):
+    """ ContinueLogin - helper for auth method login that just continues """
+    def __init__(self, user_obj, message=None):
+        LoginReturn.__init__(self, user_obj, True, message=message)
+
+class CancelLogin(LoginReturn):
+    """ CancelLogin - cancel login showing a message """
+    def __init__(self, message):
+        LoginReturn.__init__(self, None, False, message=message)
+
+class MultistageFormLogin(LoginReturn):
+    """ MultistageFormLogin - require user to fill in another form """
+    def __init__(self, multistage):
+        LoginReturn.__init__(self, None, False, multistage=multistage)
+
+class MultistageRedirectLogin(LoginReturn):
+    """ MultistageRedirectLogin - redirect user to another site before continuing login """
+    def __init__(self, url):
+        LoginReturn.__init__(self, None, False, redirect_to=url)
+
+
+class BaseAuth(object):
+    name = None
+    login_inputs = []
+    logout_possible = False
+    def __init__(self):
+        pass
+    def login(self, user_obj, **kw):
+        return ContinueLogin(user_obj)
+    def request(self, user_obj, **kw):
+        return user_obj, True
+    def logout(self, user_obj, **kw):
+        if self.name and user_obj and user_obj.auth_method == self.name:
+            logging.debug("%s: logout - invalidating user %r" % (self.name, user_obj.name))
+            user_obj.valid = False
+        return user_obj, True
+    def login_hint(self):
+        return None
+
+class MoinAuth(BaseAuth):
+    """ handle login from moin login form """
+    def __init__(self):
+        BaseAuth.__init__(self)
+
+    login_inputs = ['username', 'password']
+    name = 'moin'
+    logout_possible = True
+
+    def login(self, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
+
+        # simply continue if something else already logged in successfully
+        if user_obj and user_obj.valid:
+            return ContinueLogin(user_obj)
+
+        if not username and not password:
+            return ContinueLogin(user_obj)
+
+        logging.debug("%s: performing login action" % self.name)
+
+        if username and not password:
+            return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
+
+        u = user.User(name=username, password=password, auth_method=self.name)
+        if u.valid:
+            logging.debug("%s: successfully authenticated user %r (valid)" % (self.name, u.name))
+            return ContinueLogin(u)
+        else:
+            logging.debug("%s: could not authenticate user %r (not valid)" % (self.name, username))
+            return ContinueLogin(user_obj, _("Invalid username or password."))
+
+    def login_hint(self):
+        msg = _('If you do not have an account, <a href="%(register_url)s">you can create one now</a>. ',
+                register_url=url_for('frontend.register'))
+        msg += _('<a href="%(recover_url)s">Forgot your password?</a>',
+                 recover_url=url_for('frontend.lostpass'))
+        return Markup(msg)
+
+
+class GivenAuth(BaseAuth):
+    """ reuse a given authentication, e.g. http basic auth (or any other auth)
+        done by the web server, that sets REMOTE_USER environment variable.
+        This is the default behaviour.
+        You can also specify to read another environment variable (env_var).
+        Alternatively you can directly give a fixed user name (user_name)
+        that will be considered as authenticated.
+    """
+    name = 'given' # was 'http' in 1.8.x and before
+
+    def __init__(self,
+                 env_var=None,  # environment variable we want to read (default: REMOTE_USER)
+                 user_name=None,  # can be used to just give a specific user name to log in
+                 autocreate=False,  # create/update the user profile for the auth. user
+                 strip_maildomain=False,  # joe@example.org -> joe
+                 strip_windomain=False,  # DOMAIN\joe -> joe
+                 titlecase=False,  # joe doe -> Joe Doe
+                 remove_blanks=False,  # Joe Doe -> JoeDoe
+                 coding=None,  # for decoding REMOTE_USER correctly (default: auto)
+                ):
+        self.env_var = env_var
+        self.user_name = user_name
+        self.autocreate = autocreate
+        self.strip_maildomain = strip_maildomain
+        self.strip_windomain = strip_windomain
+        self.titlecase = titlecase
+        self.remove_blanks = remove_blanks
+        self.coding = coding
+        BaseAuth.__init__(self)
+
+    def decode_username(self, name):
+        """ decode the name we got from the environment var to unicode """
+        if isinstance(name, str):
+            if self.coding:
+                name = name.decode(self.coding)
+            else:
+                # XXX we have no idea about REMOTE_USER encoding, please help if
+                # you know how to do that cleanly
+                name = wikiutil.decodeUnknownInput(name)
+        return name
+
+    def transform_username(self, name):
+        """ transform the name we got (unicode in, unicode out)
+
+            Note: if you need something more special, you could create your own
+                  auth class, inherit from this class and overwrite this function.
+        """
+        assert isinstance(name, unicode)
+        if self.strip_maildomain:
+            # split off mail domain, e.g. "user@example.org" -> "user"
+            name = name.split(u'@')[0]
+
+        if self.strip_windomain:
+            # split off window domain, e.g. "DOMAIN\user" -> "user"
+            name = name.split(u'\\')[-1]
+
+        if self.titlecase:
+            # this "normalizes" the login name, e.g. meier, Meier, MEIER -> Meier
+            name = name.title()
+
+        if self.remove_blanks:
+            # remove blanks e.g. "Joe Doe" -> "JoeDoe"
+            name = u''.join(name.split())
+
+        return name
+
+    def request(self, user_obj, **kw):
+        u = None
+        # always revalidate auth
+        if user_obj and user_obj.auth_method == self.name:
+            user_obj = None
+        # something else authenticated before us
+        if user_obj:
+            logging.debug("already authenticated, doing nothing")
+            return user_obj, True
+
+        if self.user_name is not None:
+            auth_username = self.user_name
+        elif self.env_var is None:
+            auth_username = request.remote_user
+        else:
+            auth_username = request.environ.get(self.env_var)
+
+        logging.debug("auth_username = %r" % auth_username)
+        if auth_username:
+            auth_username = self.decode_username(auth_username)
+            auth_username = self.transform_username(auth_username)
+            logging.debug("auth_username (after decode/transform) = %r" % auth_username)
+            u = user.User(auth_username=auth_username,
+                          auth_method=self.name, auth_attribs=('name', 'password'))
+
+        logging.debug("u: %r" % u)
+        if u and self.autocreate:
+            logging.debug("autocreating user")
+            u.create_or_update()
+        if u and u.valid:
+            logging.debug("returning valid user %r" % u)
+            return u, True # True to get other methods called, too
+        else:
+            logging.debug("returning %r" % user_obj)
+            return user_obj, True
+
+
+def handle_login(userobj, **kw):
+    """
+    Process a 'login' request by going through the configured authentication
+    methods in turn. The passable keyword arguments are explained in more
+    detail at the top of this file.
+    """
+
+    stage = kw.get('stage')
+    params = {'username': kw.get('login_username'),
+              'password': kw.get('login_password'),
+              'openid': kw.get('login_openid'),
+              'multistage': (stage and True) or None,
+              'attended': True
+             }
+    # add the other parameters from the form
+    for param in kw.keys():
+        params[param] = kw.get(param)
+
+    for authmethod in app.cfg.auth:
+        if stage and authmethod.name != stage:
+            continue
+        ret = authmethod.login(userobj, **params)
+
+        userobj = ret.user_obj
+        cont = ret.continue_flag
+        if stage:
+            stage = None
+            del params['multistage']
+
+        if ret.multistage:
+            flaskg._login_multistage = ret.multistage
+            flaskg._login_multistage_name = authmethod.name
+            return userobj
+
+        if ret.redirect_to:
+            nextstage = get_multistage_continuation_url(authmethod.name)
+            url = ret.redirect_to
+            url = url.replace('%return_form', url_quote_plus(nextstage))
+            url = url.replace('%return', url_quote(nextstage))
+            abort(redirect(url))
+        msg = ret.message
+        if msg and not msg in flaskg._login_messages:
+            flaskg._login_messages.append(msg)
+
+        if not cont:
+            break
+
+    return userobj
+
+def handle_logout(userobj):
+    """ Logout the passed user from every configured authentication method. """
+    if userobj is None:
+        # not logged in
+        return userobj
+
+    for authmethod in app.cfg.auth:
+        userobj, cont = authmethod.logout(userobj)
+        if not cont:
+            break
+    return userobj
+
+def handle_request(userobj):
+    """ Handle the per-request callbacks of the configured authentication methods. """
+    for authmethod in app.cfg.auth:
+        userobj, cont = authmethod.request(userobj)
+        if not cont:
+            break
+    return userobj
+
+def setup_from_session():
+    userobj = None
+    if 'user.id' in session:
+        auth_userid = session['user.id']
+        auth_method = session['user.auth_method']
+        auth_attrs = session['user.auth_attribs']
+        logging.debug("got from session: %r %r" % (auth_userid, auth_method))
+        logging.debug("current auth methods: %r" % app.cfg.auth_methods)
+        if auth_method and auth_method in app.cfg.auth_methods:
+            userobj = user.User(auth_userid,
+                                auth_method=auth_method,
+                                auth_attribs=auth_attrs)
+    logging.debug("session started for user %r", userobj)
+    return userobj
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/_tests/test_ldap_login.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+"""
+    MoinMoin - MoinMoin.auth.ldap Tests
+
+    @copyright: 2008 MoinMoin:ThomasWaldmann,
+                2010 MoinMoin:ReimarBauer
+
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import py.test
+
+
+from MoinMoin._tests.ldap_testbase import LDAPTstBase, LdapEnvironment, check_environ, SLAPD_EXECUTABLE
+from MoinMoin._tests.ldap_testdata import *
+from MoinMoin._tests import wikiconfig
+from MoinMoin.auth import handle_login
+
+# first check if we have python 2.4, python-ldap and slapd:
+msg = check_environ()
+if msg:
+    py.test.skip(msg)
+del msg
+
+import ldap
+
+class TestLDAPServer(LDAPTstBase):
+    basedn = BASEDN
+    rootdn = ROOTDN
+    rootpw = ROOTPW
+    slapd_config = SLAPD_CONFIG
+    ldif_content = LDIF_CONTENT
+
+    def testLDAP(self):
+        """ Just try accessing the LDAP server and see if usera and userb are in LDAP. """
+        server_uri = self.ldap_env.slapd.url
+        base_dn = self.ldap_env.basedn
+        lo = ldap.initialize(server_uri)
+        ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+        lo.simple_bind_s('', '')
+        lusers = lo.search_st(base_dn, ldap.SCOPE_SUBTREE, '(uid=*)')
+        uids = [ldap_dict['uid'][0] for dn, ldap_dict in lusers]
+        assert 'usera' in uids
+        assert 'userb' in uids
+
+class TestMoinLDAPLogin(LDAPTstBase):
+    basedn = BASEDN
+    rootdn = ROOTDN
+    rootpw = ROOTPW
+    slapd_config = SLAPD_CONFIG
+    ldif_content = LDIF_CONTENT
+
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth.ldap_login import LDAPAuth
+        #ToDo get these vars from the test environment
+        server_uri = 'ldap://127.0.0.1:3890'
+        base_dn = 'ou=testing,dc=example,dc=org'
+
+        ldap_auth1 = LDAPAuth(server_uri=server_uri, base_dn=base_dn, autocreate=True)
+        auth = [ldap_auth1, ]
+
+    def testMoinLDAPLogin(self):
+        """ Just try accessing the LDAP server and see if usera and userb are in LDAP. """
+
+        # tests that must not authenticate:
+        u = handle_login(None, username='', password='')
+        assert u is None
+        u = handle_login(None, username='usera', password='')
+        assert u is None
+        u = handle_login(None, username='usera', password='userawrong')
+        assert u is None
+        u = handle_login(None, username='userawrong', password='usera')
+        assert u is None
+
+        # tests that must authenticate:
+        u1 = handle_login(None, username='usera', password='usera')
+        assert u1 is not None
+        assert u1.valid
+
+        u2 = handle_login(None, username='userb', password='userb')
+        assert u2 is not None
+        assert u2.valid
+
+        # check if usera and userb have different ids:
+        assert u1.id != u2.id
+
+
+class TestBugDefaultPasswd(LDAPTstBase):
+    basedn = BASEDN
+    rootdn = ROOTDN
+    rootpw = ROOTPW
+    slapd_config = SLAPD_CONFIG
+    ldif_content = LDIF_CONTENT
+
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth.ldap_login import LDAPAuth
+        from MoinMoin.auth import MoinAuth
+        #ToDo get these vars from the test environment
+        server_uri = 'ldap://127.0.0.1:3890'
+        base_dn = 'ou=testing,dc=example,dc=org'
+        ldap_auth = LDAPAuth(server_uri=server_uri, base_dn=base_dn, autocreate=True)
+        moin_auth = MoinAuth()
+        auth = [ldap_auth, moin_auth]
+
+    def teardown_class(self):
+        """ Stop slapd, remove LDAP server environment """
+        self.ldap_env.stop_slapd()
+        self.ldap_env.destroy_env()
+
+    def testBugDefaultPasswd(self):
+        """ Login via LDAP (this creates user profile and up to 1.7.0rc1 it put
+            a default password there), then try logging in via moin login using
+            that default password or an empty password.
+        """
+        # do a LDAPAuth login (as a side effect, this autocreates the user profile):
+        u1 = handle_login(None, username='usera', password='usera')
+        assert u1 is not None
+        assert u1.valid
+
+        # now we kill the LDAP server:
+        #self.ldap_env.slapd.stop()
+
+        # now try a MoinAuth login:
+        # try the default password that worked in 1.7 up to rc1:
+        u2 = handle_login(None, username='usera', password='{SHA}NotStored')
+        assert u2 is None
+
+        # try using no password:
+        u2 = handle_login(None, username='usera', password='')
+        assert u2 is None
+
+        # try using wrong password:
+        u2 = handle_login(None, username='usera', password='wrong')
+        assert u2 is None
+
+class TestTwoLdapServers(object):
+    basedn = BASEDN
+    rootdn = ROOTDN
+    rootpw = ROOTPW
+    slapd_config = SLAPD_CONFIG
+    ldif_content = LDIF_CONTENT
+
+    def setup_class(self):
+        """ Create LDAP servers environment, start slapds """
+        self.ldap_envs = []
+        for instance in range(2):
+            ldap_env = LdapEnvironment(self.basedn, self.rootdn, self.rootpw, instance=instance)
+            ldap_env.create_env(slapd_config=self.slapd_config)
+            started = ldap_env.start_slapd()
+            if not started:
+                py.test.skip("Failed to start %s process, please see your syslog / log files"
+                             " (and check if stopping apparmor helps, in case you use it)." % SLAPD_EXECUTABLE)
+            ldap_env.load_directory(ldif_content=self.ldif_content)
+            self.ldap_envs.append(ldap_env)
+
+    def teardown_class(self):
+        """ Stop slapd, remove LDAP server environment """
+        for ldap_env in self.ldap_envs:
+            ldap_env.stop_slapd()
+            ldap_env.destroy_env()
+
+    def testLDAP(self):
+        """ Just try accessing the LDAP servers and see if usera and userb are in LDAP. """
+        for ldap_env in self.ldap_envs:
+            server_uri = ldap_env.slapd.url
+            base_dn = ldap_env.basedn
+            lo = ldap.initialize(server_uri)
+            ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+            lo.simple_bind_s('', '')
+            lusers = lo.search_st(base_dn, ldap.SCOPE_SUBTREE, '(uid=*)')
+            uids = [ldap_dict['uid'][0] for dn, ldap_dict in lusers]
+            assert 'usera' in uids
+            assert 'userb' in uids
+
+
+class TestLdapFailover(object):
+    basedn = BASEDN
+    rootdn = ROOTDN
+    rootpw = ROOTPW
+    slapd_config = SLAPD_CONFIG
+    ldif_content = LDIF_CONTENT
+
+
+    def setup_class(self):
+        """ Create LDAP servers environment, start slapds """
+        self.ldap_envs = []
+        for instance in range(2):
+            ldap_env = LdapEnvironment(self.basedn, self.rootdn, self.rootpw, instance=instance)
+            ldap_env.create_env(slapd_config=self.slapd_config)
+            started = ldap_env.start_slapd()
+            if not started:
+                py.test.skip("Failed to start %s process, please see your syslog / log files"
+                             " (and check if stopping apparmor helps, in case you use it)." % SLAPD_EXECUTABLE)
+            ldap_env.load_directory(ldif_content=self.ldif_content)
+            self.ldap_envs.append(ldap_env)
+
+    class Config(wikiconfig.Config):
+        from MoinMoin.auth.ldap_login import LDAPAuth
+        #ToDo get these vars from the test environment
+        server_uri = 'ldap://127.0.0.1:3891'
+        base_dn = 'ou=testing,dc=example,dc=org'
+        ldap_auth1 = LDAPAuth(server_uri=server_uri, base_dn=base_dn,
+                             name="ldap1", autocreate=True,
+                             timeout=1)
+        # short timeout, faster testing
+        server_uri = 'ldap://127.0.0.1:3892'
+        ldap_auth2 = LDAPAuth(server_uri=server_uri, base_dn=base_dn,
+                             name="ldap2", autocreate=True,
+                             timeout=1)
+
+        auth = [ldap_auth1, ldap_auth2]
+
+    def teardown_class(self):
+        """ Stop slapd, remove LDAP server environment """
+        for ldap_env in self.ldap_envs:
+            try:
+                ldap_env.stop_slapd()
+            except:
+                pass # one will fail, because it is already stopped
+            ldap_env.destroy_env()
+
+    def testMoinLDAPFailOver(self):
+        """ Try if it does a failover to a secondary LDAP, if the primary fails. """
+
+        # authenticate user (with primary slapd):
+        u1 = handle_login(None, username='usera', password='usera')
+        assert u1 is not None
+        assert u1.valid
+
+        # now we kill our primary LDAP server:
+        self.ldap_envs[0].slapd.stop()
+
+        # try if we can still authenticate (with the second slapd):
+        u2 = handle_login(None, username='usera', password='usera')
+        assert u2 is not None
+        assert u2.valid
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/http.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,76 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - http authentication
+
+    HTTPAuthMoin
+    ============
+
+    HTTPAuthMoin is HTTP auth done by moin (not by your web server).
+
+    Moin will request HTTP Basic Auth and use the HTTP Basic Auth header it
+    receives to authenticate username/password against the moin user profiles.
+
+    from MoinMoin.auth.http import HTTPAuthMoin
+    auth = [HTTPAuthMoin()]
+    # check if you want 'http' auth name in there:
+    auth_methods_trusted = ['http', ]
+
+    @copyright: 2009 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from flask import request
+
+from MoinMoin import config, user
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.auth import BaseAuth, GivenAuth
+
+
+class HTTPAuthMoin(BaseAuth):
+    """ authenticate via http (basic) auth """
+    name = 'http'
+
+    def __init__(self, autocreate=False, realm='MoinMoin', coding='iso-8859-1'):
+        self.autocreate = autocreate
+        self.realm = realm
+        self.coding = coding
+        BaseAuth.__init__(self)
+
+    def request(self, user_obj, **kw):
+        u = None
+        # always revalidate auth
+        if user_obj and user_obj.auth_method == self.name:
+            user_obj = None
+        # something else authenticated before us
+        if user_obj:
+            return user_obj, True
+
+        auth = request.authorization
+        if auth and auth.username and auth.password is not None:
+            logging.debug("http basic auth, received username: %r password: %r" % (
+                          auth.username, auth.password))
+            u = user.User(name=auth.username.decode(self.coding),
+                          password=auth.password.decode(self.coding),
+                          auth_method=self.name, auth_attribs=[])
+            logging.debug("user: %r" % u)
+
+        if not u or not u.valid:
+            from werkzeug import Response, abort
+            response = Response(_('Please log in first.'), 401,
+                                {'WWW-Authenticate': 'Basic realm="%s"' % self.realm})
+            abort(response)
+
+        logging.debug("u: %r" % u)
+        if u and self.autocreate:
+            logging.debug("autocreating user")
+            u.create_or_update()
+        if u and u.valid:
+            logging.debug("returning valid user %r" % u)
+            return u, True # True to get other methods called, too
+        else:
+            logging.debug("returning %r" % user_obj)
+            return user_obj, True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/ldap_login.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,264 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - LDAP / Active Directory authentication
+
+    This code only creates a user object, the session will be established by
+    moin automatically.
+
+    python-ldap needs to be at least 2.0.0pre06 (available since mid 2002) for
+    ldaps support - some older debian installations (woody and older?) require
+    libldap2-tls and python2.x-ldap-tls, otherwise you get ldap.SERVER_DOWN:
+    "Can't contact LDAP server" - more recent debian installations have tls
+    support in libldap2 (see dependency on gnutls) and also in python-ldap.
+
+    TODO: allow more configuration (alias name, ...) by using callables as parameters
+
+    @copyright: 2006-2008 MoinMoin:ThomasWaldmann,
+                2006 Nick Phillips
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+try:
+    import ldap
+except ImportError, err:
+    logging.error("You need to have python-ldap installed (%s)." % str(err))
+    raise
+
+from MoinMoin import user
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin
+
+
+class LDAPAuth(BaseAuth):
+    """ get authentication data from form, authenticate against LDAP (or Active
+        Directory), fetch some user infos from LDAP and create a user object
+        for that user. The session is kept by moin automatically.
+    """
+
+    login_inputs = ['username', 'password']
+    logout_possible = True
+    name = 'ldap'
+
+    def __init__(self,
+        server_uri='ldap://localhost',  # ldap / active directory server URI
+                                        # use ldaps://server:636 url for ldaps,
+                                        # use  ldap://server for ldap without tls (and set start_tls to 0),
+                                        # use  ldap://server for ldap with tls (and set start_tls to 1 or 2).
+        bind_dn='',  # We can either use some fixed user and password for binding to LDAP.
+                     # Be careful if you need a % char in those strings - as they are used as
+                     # a format string, you have to write %% to get a single % in the end.
+                     #bind_dn = 'binduser@example.org' # (AD)
+                     #bind_dn = 'cn=admin,dc=example,dc=org' # (OpenLDAP)
+                     #bind_pw = 'secret'
+                     # or we can use the username and password we got from the user:
+                     #bind_dn = '%(username)s@example.org' # DN we use for first bind (AD)
+                     #bind_pw = '%(password)s' # password we use for first bind
+                     # or we can bind anonymously (if that is supported by your directory).
+                     # In any case, bind_dn and bind_pw must be defined.
+        bind_pw='',
+        base_dn='',  # base DN we use for searching
+                     #base_dn = 'ou=SOMEUNIT,dc=example,dc=org'
+        scope=ldap.SCOPE_SUBTREE, # scope of the search we do (2 == ldap.SCOPE_SUBTREE)
+        referrals=0, # LDAP REFERRALS (0 needed for AD)
+        search_filter='(uid=%(username)s)',  # ldap filter used for searching:
+                                             #search_filter = '(sAMAccountName=%(username)s)' # (AD)
+                                             #search_filter = '(uid=%(username)s)' # (OpenLDAP)
+                                             # you can also do more complex filtering like:
+                                             # "(&(cn=%(username)s)(memberOf=CN=WikiUsers,OU=Groups,DC=example,DC=org))"
+        # some attribute names we use to extract information from LDAP:
+        givenname_attribute=None, # ('givenName') ldap attribute we get the first name from
+        surname_attribute=None, # ('sn') ldap attribute we get the family name from
+        aliasname_attribute=None, # ('displayName') ldap attribute we get the aliasname from
+        email_attribute=None, # ('mail') ldap attribute we get the email address from
+        email_callback=None, # called to make up email address
+        name_callback=None, # called to use a Wiki name different from the login name
+        coding='utf-8', # coding used for ldap queries and result values
+        timeout=10, # how long we wait for the ldap server [s]
+        start_tls=0, # 0 = No, 1 = Try, 2 = Required
+        tls_cacertdir=None,
+        tls_cacertfile=None,
+        tls_certfile=None,
+        tls_keyfile=None,
+        tls_require_cert=0, # 0 == ldap.OPT_X_TLS_NEVER (needed for self-signed certs)
+        bind_once=False, # set to True to only do one bind - useful if configured to bind as the user on the first attempt
+        autocreate=False, # set to True if you want to autocreate user profiles
+        name='ldap', # use e.g. 'ldap_pdc' and 'ldap_bdc' (or 'ldap1' and 'ldap2') if you auth against 2 ldap servers
+        report_invalid_credentials=True, # whether to emit "invalid username or password" msg at login time or not
+        ):
+        self.server_uri = server_uri
+        self.bind_dn = bind_dn
+        self.bind_pw = bind_pw
+        self.base_dn = base_dn
+        self.scope = scope
+        self.referrals = referrals
+        self.search_filter = search_filter
+
+        self.givenname_attribute = givenname_attribute
+        self.surname_attribute = surname_attribute
+        self.aliasname_attribute = aliasname_attribute
+        self.email_attribute = email_attribute
+        self.email_callback = email_callback
+        self.name_callback = name_callback
+
+        self.coding = coding
+        self.timeout = timeout
+
+        self.start_tls = start_tls
+        self.tls_cacertdir = tls_cacertdir
+        self.tls_cacertfile = tls_cacertfile
+        self.tls_certfile = tls_certfile
+        self.tls_keyfile = tls_keyfile
+        self.tls_require_cert = tls_require_cert
+
+        self.bind_once = bind_once
+        self.autocreate = autocreate
+        self.name = name
+
+        self.report_invalid_credentials = report_invalid_credentials
+
+    def login(self, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
+
+
+        # we require non-empty password as ldap bind does a anon (not password
+        # protected) bind if the password is empty and SUCCEEDS!
+        if not password:
+            return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
+
+        try:
+            try:
+                u = None
+                dn = None
+                coding = self.coding
+                logging.debug("Setting misc. ldap options...")
+                ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) # ldap v2 is outdated
+                ldap.set_option(ldap.OPT_REFERRALS, self.referrals)
+                ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
+
+                if hasattr(ldap, 'TLS_AVAIL') and ldap.TLS_AVAIL:
+                    for option, value in (
+                        (ldap.OPT_X_TLS_CACERTDIR, self.tls_cacertdir),
+                        (ldap.OPT_X_TLS_CACERTFILE, self.tls_cacertfile),
+                        (ldap.OPT_X_TLS_CERTFILE, self.tls_certfile),
+                        (ldap.OPT_X_TLS_KEYFILE, self.tls_keyfile),
+                        (ldap.OPT_X_TLS_REQUIRE_CERT, self.tls_require_cert),
+                        (ldap.OPT_X_TLS, self.start_tls),
+                        #(ldap.OPT_X_TLS_ALLOW, 1),
+                    ):
+                        if value is not None:
+                            ldap.set_option(option, value)
+
+                server = self.server_uri
+                logging.debug("Trying to initialize %r." % server)
+                l = ldap.initialize(server)
+                logging.debug("Connected to LDAP server %r." % server)
+
+                if self.start_tls and server.startswith('ldap:'):
+                    logging.debug("Trying to start TLS to %r." % server)
+                    try:
+                        l.start_tls_s()
+                        logging.debug("Using TLS to %r." % server)
+                    except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR), err:
+                        logging.warning("Couldn't establish TLS to %r (err: %s)." % (server, str(err)))
+                        raise
+
+                # you can use %(username)s and %(password)s here to get the stuff entered in the form:
+                binddn = self.bind_dn % locals()
+                bindpw = self.bind_pw % locals()
+                l.simple_bind_s(binddn.encode(coding), bindpw.encode(coding))
+                logging.debug("Bound with binddn %r" % binddn)
+
+                # you can use %(username)s here to get the stuff entered in the form:
+                filterstr = self.search_filter % locals()
+                logging.debug("Searching %r" % filterstr)
+                attrs = [getattr(self, attr) for attr in [
+                                         'email_attribute',
+                                         'aliasname_attribute',
+                                         'surname_attribute',
+                                         'givenname_attribute',
+                                         ] if getattr(self, attr) is not None]
+                lusers = l.search_st(self.base_dn, self.scope, filterstr.encode(coding),
+                                     attrlist=attrs, timeout=self.timeout)
+                # we remove entries with dn == None to get the real result list:
+                lusers = [(dn, ldap_dict) for dn, ldap_dict in lusers if dn is not None]
+                for dn, ldap_dict in lusers:
+                    logging.debug("dn:%r" % dn)
+                    for key, val in ldap_dict.items():
+                        logging.debug("    %r: %r" % (key, val))
+
+                result_length = len(lusers)
+                if result_length != 1:
+                    if result_length > 1:
+                        logging.warning("Search found more than one (%d) matches for %r." % (result_length, filterstr))
+                    if result_length == 0:
+                        logging.debug("Search found no matches for %r." % (filterstr, ))
+                    if self.report_invalid_credentials:
+                        return ContinueLogin(user_obj, _("Invalid username or password."))
+                    else:
+                        return ContinueLogin(user_obj)
+
+                dn, ldap_dict = lusers[0]
+                if not self.bind_once:
+                    logging.debug("DN found is %r, trying to bind with pw" % dn)
+                    l.simple_bind_s(dn, password.encode(coding))
+                    logging.debug("Bound with dn %r (username: %r)" % (dn, username))
+
+                if self.email_callback is None:
+                    if self.email_attribute:
+                        email = ldap_dict.get(self.email_attribute, [''])[0].decode(coding)
+                    else:
+                        email = None
+                else:
+                    email = self.email_callback(ldap_dict)
+
+                aliasname = ''
+                try:
+                    aliasname = ldap_dict[self.aliasname_attribute][0]
+                except (KeyError, IndexError):
+                    pass
+                if not aliasname:
+                    sn = ldap_dict.get(self.surname_attribute, [''])[0]
+                    gn = ldap_dict.get(self.givenname_attribute, [''])[0]
+                    if sn and gn:
+                        aliasname = "%s, %s" % (sn, gn)
+                    elif sn:
+                        aliasname = sn
+                aliasname = aliasname.decode(coding)
+
+                if self.name_callback:
+                    username = self.name_callback(ldap_dict)
+
+                if email:
+                    u = user.User(auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author', ))
+                    u.email = email
+                else:
+                    u = user.User(auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'mailto_author', ))
+                u.name = username
+                u.aliasname = aliasname
+                logging.debug("creating user object with name %r email %r alias %r" % (username, email, aliasname))
+
+            except ldap.INVALID_CREDENTIALS, err:
+                logging.debug("invalid credentials (wrong password?) for dn %r (username: %r)" % (dn, username))
+                return CancelLogin(_("Invalid username or password."))
+
+            if u and self.autocreate:
+                logging.debug("calling create_or_update to autocreate user %r" % u.name)
+                u.create_or_update(True)
+            return ContinueLogin(u)
+
+        except ldap.SERVER_DOWN, err:
+            # looks like this LDAP server isn't working, so we just try the next
+            # authenticator object in cfg.auth list (there could be some second
+            # ldap authenticator that queries a backup server or any other auth
+            # method).
+            logging.error("LDAP server %s failed (%s). "
+                          "Trying to authenticate with next auth list entry." % (server, str(err)))
+            return ContinueLogin(user_obj, _("LDAP server %(server)s failed.", server=server))
+
+        except:
+            logging.exception("caught an exception, traceback follows...")
+            return ContinueLogin(user_obj)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/log.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,34 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - logging auth plugin
+
+    This does nothing except logging the auth parameters (the password is NOT
+    logged, of course).
+
+    @copyright: 2006-2008 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin.auth import BaseAuth, ContinueLogin
+
+class AuthLog(BaseAuth):
+    """ just log the call, do nothing else """
+    name = "log"
+
+    def log(self, action, user_obj, kw):
+        logging.info('%s: user_obj=%r kw=%r' % (action, user_obj, kw))
+
+    def login(self, user_obj, **kw):
+        self.log('login', user_obj, kw)
+        return ContinueLogin(user_obj)
+
+    def request(self, user_obj, **kw):
+        self.log('session', user_obj, kw)
+        return user_obj, True
+
+    def logout(self, user_obj, **kw):
+        self.log('logout', user_obj, kw)
+        return user_obj, True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/openidrp.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,164 @@
+"""
+    MoinMoin - OpenID authentication
+
+    This code handles login requests for openid multistage authentication.
+
+    @copyright: 2010 MoinMoin:Nichita Utiu
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from openid.store.memstore import MemoryStore
+from openid.consumer import consumer
+from openid.yadis.discover import DiscoveryFailure
+from openid.fetchers import HTTPFetchingError
+
+from flask import session, request, url_for
+from flask import current_app as app
+from MoinMoin.auth import BaseAuth, get_multistage_continuation_url
+from MoinMoin.auth import ContinueLogin, CancelLogin, MultistageFormLogin, MultistageRedirectLogin
+from MoinMoin import user
+from MoinMoin.i18n import _, L_, N_
+
+
+class OpenIDAuth(BaseAuth):
+    def __init__(self, trusted_providers=[]):
+        # the name
+        self.name = 'openid'
+        # we only need openid
+        self.login_inputs = ['openid']
+        # logout is possible
+        self.logout_possible = True
+        # the store
+        self.store = MemoryStore()
+
+        self._trusted_providers = list(trusted_providers)
+        BaseAuth.__init__(self)
+
+    def _handleContinuationVerify(self):
+        """
+        Handles the first stage continuation.
+        """
+        # the consumer object with an in-memory storage
+        oid_consumer = consumer.Consumer(session, self.store)
+
+        # a dict containing the parsed query string
+        query = {}
+        for key in request.values.keys():
+            query[key] = request.values.get(key)
+        # the current url (w/o query string)
+        url = get_multistage_continuation_url(self.name, {'oidstage': '1'})
+
+        # we get the info about the authentication
+        oid_info = oid_consumer.complete(query, url)
+        # the identity we've retrieved from the response
+        if oid_info.status == consumer.FAILURE:
+            # verification has failed
+            # return an error message with description of error
+            logging.debug("OpenIDError: %s" % oid_info.message)
+
+            error_message = _('OpenID Error')
+            return CancelLogin(error_message)
+        elif oid_info.status == consumer.CANCEL:
+            logging.debug("OpenID verification cancelled.")
+
+            # verification was cancelled
+            # return error
+            return CancelLogin(_('OpenID verification cancelled.'))
+        elif oid_info.status == consumer.SUCCESS:
+            logging.debug('OpenID success. id: %s' % oid_info.identity_url)
+
+            # we get the provider's url
+            # and the list of trusted providers
+            trusted = self._trusted_providers
+            server = oid_info.endpoint.server_url
+
+            if server in trusted or not trusted:
+                # the provider is trusted or all providers are trusted
+                # we have successfully authenticated our openid
+                # we get the user with this openid associated to him
+                identity = oid_info.identity_url
+                user_obj = user.get_by_openid(identity)
+
+                # if the user actually exists
+                if user_obj:
+                    # we get the authenticated user object
+                    # success!
+                    user_obj.auth_method = self.name
+                    return ContinueLogin(user_obj)
+
+                # there is no user with this openid
+                else:
+                    # redirect the user to registration
+                    return MultistageRedirectLogin(url_for('frontend.register',
+                                                           _external=True,
+                                                           openid_openid=identity,
+                                                           openid_submit='1'
+                                                          ))
+
+
+            # not trusted
+            return ContinueLogin(None, _('This OpenID provider is not trusted.'))
+
+        else:
+            logging.debug("OpenID failure")
+            # the auth failed miserably
+            return CancelLogin(_('OpenID failure.'))
+
+    def _handleContinuation(self):
+        """
+        Handles continuations appropriately.
+        """
+        # the current stage
+        oidstage = request.values.get('oidstage')
+        if oidstage == '1':
+            return self._handleContinuationVerify()
+        # more can be added for extended functionality
+
+    def login(self, userobj, **kw):
+        """
+        Handles an login request and continues to multistage continuation
+        if necessary.
+        """
+        continuation = kw.get('multistage')
+        # process another subsequent step
+        if continuation:
+            return self._handleContinuation()
+
+        openid = kw.get('openid')
+        # no openid entered
+        if not openid:
+            return ContinueLogin(userobj)
+
+        # we make a consumer object with an in-memory storage
+        oid_consumer = consumer.Consumer(session, self.store)
+
+        # we catch any possible openid-related exceptions
+        try:
+            oid_response = oid_consumer.begin(openid)
+        except HTTPFetchingError:
+            return ContinueLogin(None, _('Failed to resolve OpenID.'))
+        except DiscoveryFailure:
+            return ContinueLogin(None, _('OpenID discovery failure, not a valid OpenID.'))
+        else:
+            # we got no response from the service
+            if oid_response is None:
+                return ContinueLogin(None, _('No OpenID service at this URL.'))
+
+            # site root and where to return after the redirect
+            site_root = url_for('frontend.show_root', _external=True)
+            return_to = get_multistage_continuation_url(self.name, {'oidstage': '1'})
+
+            # should we redirect the user?
+            if oid_response.shouldSendRedirect():
+                redirect_url = oid_response.redirectURL(site_root, return_to)
+                return MultistageRedirectLogin(redirect_url)
+            else:
+                # send a form
+                form_html = oid_response.htmlMarkup(site_root, return_to, form_tag_attrs={'id': 'openid_message'})
+
+                # returns a MultistageFormLogin
+                return MultistageFormLogin(form_html)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/auth/smb_mount.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,96 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - auth plugin for (un)mounting a smb share
+
+    (u)mount a SMB server's share for username (using username/password for
+    authentication at the SMB server). This can be used if you need access
+    to files on some share via the wiki, but needs more code to be useful.
+
+    @copyright: 2006-2008 MoinMoin:ThomasWaldmann
+                2007 MoinMoin:JohannesBerg
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin
+
+class SMBMount(BaseAuth):
+    """ auth plugin for (un)mounting an smb share,
+        this is a wrapper around mount.cifs -o <options> //server/share mountpoint
+
+        See man mount.cifs for details.
+    """
+    def __init__(self,
+        server, # mount.cifs //server/share
+        share, # mount.cifs //server/share
+        mountpoint_fn, # function of username to determine the mountpoint, e.g.:
+                       # lambda username: u'/mnt/wiki/%s' % username
+        dir_user, # username to get the uid that is used for mount.cifs -o uid=... (e.g. 'www-data')
+        domain, # mount.cifs -o domain=...
+        dir_mode='0700', # mount.cifs -o dir_mode=...
+        file_mode='0600', # mount.cifs -o file_mode=...
+        iocharset='utf-8', # mount.cifs -o iocharset=... (try 'iso8859-1' if default does not work)
+        coding='utf-8', # encoding used for username/password/cmdline (try 'iso8859-1' if default does not work)
+        log='/dev/null', # logfile for mount.cifs output
+        ):
+        BaseAuth.__init__(self)
+        self.server = server
+        self.share = share
+        self.mountpoint_fn = mountpoint_fn
+        self.dir_user = dir_user
+        self.domain = domain
+        self.dir_mode = dir_mode
+        self.file_mode = file_mode
+        self.iocharset = iocharset
+        self.log = log
+        self.coding = coding
+
+    def do_smb(self, username, password, login):
+        logging.debug("login=%s logout=%s: got name=%s" % (login, not login, username))
+
+        import os, pwd, subprocess
+        web_username = self.dir_user
+        web_uid = pwd.getpwnam(web_username)[2] # XXX better just use current uid?
+
+        mountpoint = self.mountpoint_fn(username)
+        if login:
+            cmd = u"sudo mount -t cifs -o user=%(user)s,domain=%(domain)s,uid=%(uid)d,dir_mode=%(dir_mode)s,file_mode=%(file_mode)s,iocharset=%(iocharset)s //%(server)s/%(share)s %(mountpoint)s >>%(log)s 2>&1"
+        else:
+            cmd = u"sudo umount %(mountpoint)s >>%(log)s 2>&1"
+
+        cmd = cmd % {
+            'user': username,
+            'uid': web_uid,
+            'domain': self.domain,
+            'server': self.server,
+            'share': self.share,
+            'mountpoint': mountpoint,
+            'dir_mode': self.dir_mode,
+            'file_mode': self.file_mode,
+            'iocharset': self.iocharset,
+            'log': self.log,
+        }
+        env = os.environ.copy()
+        if login:
+            try:
+                if not os.path.exists(mountpoint):
+                    os.makedirs(mountpoint) # the dir containing the mountpoint must be writeable for us!
+            except OSError:
+                pass
+            env['PASSWD'] = password.encode(self.coding)
+        subprocess.call(cmd.encode(self.coding), env=env, shell=True)
+
+    def login(self, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
+        if user_obj and user_obj.valid:
+            self.do_smb(username, password, True)
+        return ContinueLogin(user_obj)
+
+    def logout(self, user_obj, **kw):
+        if user_obj and not user_obj.valid:
+            self.do_smb(user_obj.name, None, False)
+        return user_obj, True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/config/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,73 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - site-wide configuration defaults (NOT per single wiki!)
+
+    @copyright: 2005-2006 MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+import re
+
+# unicode: set the char types (upper, lower, digits, spaces)
+from MoinMoin.util.chartypes import *
+
+# Parser to use mimetype text
+parser_text_mimetype = ('plain', 'csv', 'rst', 'docbook', 'latex', 'tex', 'html', 'css',
+                       'xml', 'python', 'perl', 'php', 'ruby', 'javascript',
+                       'cplusplus', 'java', 'pascal', 'diff', 'gettext', 'xslt', 'creole', )
+
+# When creating files, we use e.g. 0666 & config.umask for the mode:
+umask = 0770
+
+# Charset - we support only 'utf-8'. While older encodings might work,
+# we don't have the resources to test them, and there is no real
+# benefit for the user. IMPORTANT: use only lowercase 'utf-8'!
+charset = 'utf-8'
+
+# Invalid characters - invisible characters that should not be in page
+# names. Prevent user confusion and wiki abuse, e.g u'\u202aFrontPage'.
+page_invalid_chars_regex = re.compile(
+    ur"""
+    \u0000 | # NULL
+
+    # Bidi control characters
+    \u202A | # LRE
+    \u202B | # RLE
+    \u202C | # PDF
+    \u202D | # LRM
+    \u202E   # RLM
+    """,
+    re.UNICODE | re.VERBOSE
+    )
+
+# used for wikiutil.clean_input
+clean_input_translation_map = {
+    # these chars will be replaced by blanks
+    ord(u'\t'): u' ',
+    ord(u'\r'): u' ',
+    ord(u'\n'): u' ',
+}
+for c in u'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f' \
+          '\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f':
+    # these chars will be removed
+    clean_input_translation_map[ord(c)] = None
+del c
+
+# Other stuff
+url_schemas = ['http', 'https', 'ftp', 'file',
+               'mailto', 'nntp', 'news',
+               'ssh', 'telnet', 'irc', 'ircs', 'xmpp', 'mumble',
+               'webcal', 'ed2k', 'apt', 'rootz',
+               'gopher',
+               'notes',
+               'rtp', 'rtsp', 'rtcp',
+              ]
+
+
+# rights that are valid in moin2
+ADMIN = 'admin'
+READ = 'read'
+WRITE = 'write'
+CREATE = 'create'
+DESTROY = 'destroy'
+ACL_RIGHTS_VALID = [READ, WRITE, CREATE, ADMIN, DESTROY, ]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/config/_tests/test_defaultconfig.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,40 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinMoin.config.default Tests
+
+    @copyright: 2007 by MoinMoin:ThomasWaldmann
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import py
+
+from flask import current_app as app
+
+class TestPasswordChecker(object):
+    username = u"SomeUser"
+    tests_builtin = [
+        (u'', False), # empty
+        (u'1966', False), # too short
+        (u'asdfghjk', False), # keyboard sequence
+        (u'QwertZuiop', False), # german keyboard sequence, with uppercase
+        (u'mnbvcx', False), # reverse keyboard sequence
+        (u'12345678', False), # keyboard sequence, too easy
+        (u'aaaaaaaa', False), # not enough different chars
+        (u'BBBaaaddd', False), # not enough different chars
+        (username, False), # username == password
+        (username[1:-1], False), # password in username
+        (u"XXX%sXXX" % username, False), # username in password
+        (u'Moin-2007', True), # this should be OK
+    ]
+    def testBuiltinPasswordChecker(self):
+        pw_checker = app.cfg.password_checker
+        if not pw_checker:
+            py.test.skip("password_checker is disabled in the configuration, not testing it")
+        else:
+            for pw, result in self.tests_builtin:
+                pw_error = pw_checker(self.username, pw)
+                print "%r: %s" % (pw, pw_error)
+                assert result == (pw_error is None)
+
+coverage_modules = ['MoinMoin.config.default']
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/config/default.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,599 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Configuration defaults class
+
+    @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
+                2005-2010 MoinMoin:ThomasWaldmann,
+                2008      MoinMoin:JohannesBerg,
+                2010      MoinMoin:DiogenesAugusto
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import re
+import os
+import sys
+
+from babel import parse_locale
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin import config, error, util
+from MoinMoin import datastruct
+from MoinMoin.auth import MoinAuth
+import MoinMoin.auth as authmodule
+from MoinMoin.security import AccessControlList
+from MoinMoin.util import plugins
+
+
+class CacheClass(object):
+    """ just a container for stuff we cache """
+    pass
+
+
+class ConfigFunctionality(object):
+    """ Configuration base class with config class behaviour.
+
+        This class contains the functionality for the DefaultConfig
+        class for the benefit of the WikiConfig macro.
+    """
+
+    # attributes of this class that should not be shown
+    # in the WikiConfig() macro.
+    siteid = None
+    cache = None
+    mail_enabled = None
+    auth_can_logout = None
+    auth_have_login = None
+    auth_login_inputs = None
+    _site_plugin_lists = None
+    xapian_searchers = None
+
+    def __init__(self):
+        """ Init Config instance """
+        self.cache = CacheClass()
+
+        if self.config_check_enabled:
+            self._config_check()
+
+        # define directories
+        data_dir = os.path.normpath(self.data_dir)
+        self.data_dir = data_dir
+        if not getattr(self, 'plugin_dir', None):
+            setattr(self, 'plugin_dir', os.path.abspath(os.path.join(data_dir, 'plugin')))
+        if not getattr(self, 'xapian_index_dir', None):
+            setattr(self, 'xapian_index_dir', os.path.abspath(os.path.join(data_dir, 'xapian')))
+
+        # Try to decode certain names which allow unicode
+        self._decode()
+
+        # After that, pre-compile some regexes
+        self.cache.item_dict_regex = re.compile(self.item_dict_regex, re.UNICODE)
+        self.cache.item_group_regex = re.compile(self.item_group_regex, re.UNICODE)
+
+        # the ..._regexact versions only match if nothing is left (exact match)
+        self.cache.item_dict_regexact = re.compile(u'^%s$' % self.item_dict_regex, re.UNICODE)
+        self.cache.item_group_regexact = re.compile(u'^%s$' % self.item_group_regex, re.UNICODE)
+
+        if not isinstance(self.superusers, list):
+            msg = """The superusers setting in your wiki configuration is not
+                    a list (e.g. ['Sample User', 'AnotherUser']).  Please change
+                    it in your wiki configuration and try again."""
+            raise error.ConfigurationError(msg)
+
+        plugins._loadPluginModule(self)
+
+        if self.user_defaults['timezone'] is None:
+            self.user_defaults['timezone'] = self.timezone_default
+        if self.user_defaults['theme_name'] is None:
+            self.user_defaults['theme_name'] = self.theme_default
+        # Note: do not assign user_defaults['locale'] = locale_default
+        # to give browser language detection a chance.
+        try:
+            self.language_default = parse_locale(self.locale_default)[0]
+        except ValueError:
+            raise error.ConfigurationError("Invalid locale_default value (give something like 'en_US').")
+
+        # post process
+        self.auth_can_logout = []
+        self.auth_login_inputs = []
+        found_names = []
+        for auth in self.auth:
+            if not auth.name:
+                raise error.ConfigurationError("Auth methods must have a name.")
+            if auth.name in found_names:
+                raise error.ConfigurationError("Auth method names must be unique.")
+            found_names.append(auth.name)
+            if auth.logout_possible and auth.name:
+                self.auth_can_logout.append(auth.name)
+            for input in auth.login_inputs:
+                if not input in self.auth_login_inputs:
+                    self.auth_login_inputs.append(input)
+        self.auth_have_login = len(self.auth_login_inputs) > 0
+        self.auth_methods = found_names
+
+        # internal dict for plugin `modules' lists
+        self._site_plugin_lists = {}
+
+        # we replace any string placeholders with config values
+        # e.g u'%(item_root)s' % self
+        self.navi_bar = [elem % self for elem in self.navi_bar]
+
+        # check if python-xapian is installed
+        if self.xapian_search:
+            try:
+                import xapian
+            except ImportError, err:
+                self.xapian_search = False
+                logging.error("xapian_search was auto-disabled because python-xapian is not installed [%s]." % str(err))
+
+        # list to cache xapian searcher objects
+        self.xapian_searchers = []
+
+        # check if mail is possible and set flag:
+        self.mail_enabled = (self.mail_smarthost is not None or self.mail_sendmail is not None) and self.mail_from
+        self.mail_enabled = self.mail_enabled and True or False
+
+        if self.namespace_mapping is None:
+            raise error.ConfigurationError("No storage configuration specified! You need to define a namespace_mapping. " + \
+                                           "For further reference, please see HelpOnStorageConfiguration.")
+
+        if self.secrets is None:  # admin did not setup a real secret
+            raise error.ConfigurationError("No secret configured! You need to set secrets = 'somelongsecretstring' in your wiki config.")
+
+        secret_key_names = ['security/ticket', ]
+        if self.textchas:
+            secret_key_names.append('security/textcha')
+
+        secret_min_length = 10
+        if isinstance(self.secrets, str):
+            if len(self.secrets) < secret_min_length:
+                raise error.ConfigurationError("The secrets = '...' wiki config setting is a way too short string (minimum length is %d chars)!" % (
+                    secret_min_length))
+            # for lazy people: set all required secrets to same value
+            secrets = {}
+            for key in secret_key_names:
+                secrets[key] = self.secrets
+            self.secrets = secrets
+
+        # we check if we have all secrets we need and that they have minimum length
+        for secret_key_name in secret_key_names:
+            try:
+                secret = self.secrets[secret_key_name]
+                if len(secret) < secret_min_length:
+                    raise ValueError
+            except (KeyError, ValueError):
+                raise error.ConfigurationError("You must set a (at least %d chars long) secret string for secrets['%s']!" % (
+                    secret_min_length, secret_key_name))
+
+    def _config_check(self):
+        """ Check namespace and warn about unknown names
+
+        Warn about names which are not used by DefaultConfig, except
+        modules, classes, _private or __magic__ names.
+
+        This check is disabled by default, when enabled, it will show an
+        error message with unknown names.
+        """
+        unknown = ['"%s"' % name for name in dir(self)
+                  if not name.startswith('_') and
+                  name not in DefaultConfig.__dict__ and
+                  not isinstance(getattr(self, name), (type(sys), type(DefaultConfig)))]
+        if unknown:
+            msg = """
+Unknown configuration options: %s.
+
+For more information, visit HelpOnConfiguration. Please check your
+configuration for typos before requesting support or reporting a bug.
+""" % ', '.join(unknown)
+            raise error.ConfigurationError(msg)
+
+    def _decode(self):
+        """ Try to decode certain names, ignore unicode values
+
+        Try to decode str using utf-8. If the decode fail, raise FatalError.
+
+        Certain config variables should contain unicode values, and
+        should be defined with u'text' syntax. Python decode these if
+        the file have a 'coding' line.
+
+        This will allow utf-8 users to use simple strings using, without
+        using u'string'. Other users will have to use u'string' for
+        these names, because we don't know what is the charset of the
+        config files.
+        """
+        charset = 'utf-8'
+        message = u"""
+"%(name)s" configuration variable is a string, but should be
+unicode. Use %(name)s = u"value" syntax for unicode variables.
+
+Also check your "-*- coding -*-" line at the top of your configuration
+file. It should match the actual charset of the configuration file.
+"""
+
+        decode_names = (
+            'sitename', 'interwikiname', 'user_homewiki', 'navi_bar',
+            'interwiki_preferred',
+            'item_root', 'item_license', 'mail_from',
+            'item_dict_regex', 'item_group_regex',
+            'superusers', 'textchas_disabled_group', 'supplementation_item_names', 'html_pagetitle',
+            'theme_default', 'timezone_default', 'locale_default',
+        )
+
+        for name in decode_names:
+            attr = getattr(self, name, None)
+            if attr is not None:
+                # Try to decode strings
+                if isinstance(attr, str):
+                    try:
+                        setattr(self, name, unicode(attr, charset))
+                    except UnicodeError:
+                        raise error.ConfigurationError(message %
+                                                       {'name': name})
+                # Look into lists and try to decode strings inside them
+                elif isinstance(attr, list):
+                    for i in xrange(len(attr)):
+                        item = attr[i]
+                        if isinstance(item, str):
+                            try:
+                                attr[i] = unicode(item, charset)
+                            except UnicodeError:
+                                raise error.ConfigurationError(message %
+                                                               {'name': name})
+
+    def __getitem__(self, item):
+        """ Make it possible to access a config object like a dict """
+        return getattr(self, item)
+
+
+class DefaultConfig(ConfigFunctionality):
+    """ Configuration base class with default config values
+        (added below)
+    """
+    # Do not add anything into this class. Functionality must
+    # be added above to avoid having the methods show up in
+    # the WikiConfig macro. Settings must be added below to
+    # the options dictionary.
+
+
+def _default_password_checker(cfg, username, password):
+    """ Check if a password is secure enough.
+        We use a built-in check to get rid of the worst passwords.
+
+        We do NOT use cracklib / python-crack here any more because it is
+        not thread-safe (we experienced segmentation faults when using it).
+
+        If you don't want to check passwords, use password_checker = None.
+
+        @return: None if there is no problem with the password,
+                 some unicode object with an error msg, if the password is problematic.
+    """
+    # in any case, do a very simple built-in check to avoid the worst passwords
+    if len(password) < 6:
+        return _("Password is too short.")
+    if len(set(password)) < 4:
+        return _("Password has not enough different characters.")
+
+    username_lower = username.lower()
+    password_lower = password.lower()
+    if username in password or password in username or \
+       username_lower in password_lower or password_lower in username_lower:
+        return _("Password is too easy to guess (password contains name or name contains password).")
+
+    keyboards = (ur"`1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./", # US kbd
+                 ur"^1234567890ߴqwertzuiop+asdfghjkl#yxcvbnm,.-", # german kbd
+                ) # add more keyboards!
+    for kbd in keyboards:
+        rev_kbd = kbd[::-1]
+        if password in kbd or password in rev_kbd or \
+           password_lower in kbd or password_lower in rev_kbd:
+            return _("Password is too easy to guess (keyboard sequence).")
+    return None
+
+
+class DefaultExpression(object):
+    def __init__(self, exprstr):
+        self.text = exprstr
+        self.value = eval(exprstr)
+
+
+#
+# Options that are not prefixed automatically with their
+# group name, see below (at the options dict) for more
+# information on the layout of this structure.
+#
+options_no_group_name = {
+  # ==========================================================================
+  'datastruct': ('Datastruct settings', None, (
+    #('dicts', lambda cfg: datastruct.ConfigDicts({}),
+    ('dicts', lambda cfg: datastruct.WikiDicts(),
+     "function f(cfg) that returns a backend which is used to access dicts definitions."),
+    #('groups', lambda cfg: datastruct.ConfigGroups({}),
+    ('groups', lambda cfg: datastruct.WikiGroups(),
+     "function f(cfg) that returns a backend which is used to access groups definitions."),
+  )),
+  # ==========================================================================
+  'auth': ('Authentication / Authorization / Security settings', None, (
+    ('superusers', [],
+     "List of trusted user names [Unicode] with wiki system administration super powers (not to be confused with ACL admin rights!). Used for e.g. software installation, language installation via SystemPagesSetup and more. See also HelpOnSuperUser."),
+    ('auth', DefaultExpression('[MoinAuth()]'),
+     "list of auth objects, to be called in this order (see HelpOnAuthentication)"),
+    ('auth_methods_trusted', ['http', 'given', ], # Note: 'http' auth method is currently just a redirect to 'given'
+     'authentication methods for which users should be included in the special "Trusted" ACL group.'),
+    ('secrets', None, """Either a long shared secret string used for multiple purposes or a dict {"purpose": "longsecretstring", ...} for setting up different shared secrets for different purposes."""),
+    ('SecurityPolicy',
+     None,
+     "Class object hook for implementing security restrictions or relaxations"),
+    ('actions_excluded',
+     ['copy',  # has questionable behaviour regarding subpages a user can't read, but can copy
+     ],
+     "Exclude unwanted actions (list of strings)"),
+
+    ('password_checker', DefaultExpression('_default_password_checker'),
+     'checks whether a password is acceptable (default check is length >= 6, at least 4 different chars, no keyboard sequence, not username used somehow (you can switch this off by using `None`)'),
+
+  )),
+  # ==========================================================================
+  'spam_leech_dos': ('Anti-Spam/Leech/DOS',
+  'These settings help limiting ressource usage and avoiding abuse.',
+  (
+    ('textchas', None,
+     "Spam protection setup using site-specific questions/answers, see HelpOnSpam."),
+    ('textchas_disabled_group', None,
+     "Name of a group of trusted users who do not get asked !TextCha questions. [Unicode]"),
+    ('textchas_expiry_time', 600,
+     "Time [s] for a !TextCha to expire."),
+  )),
+  # ==========================================================================
+  'style': ('Style / Theme / UI related',
+  'These settings control how the wiki user interface will look like.',
+  (
+    ('sitename', u'Untitled Wiki',
+     "Short description of your wiki site, displayed below the logo on each page, and used in RSS documents as the channel title [Unicode]"),
+    ('interwikiname', None, "unique and stable InterWiki name (prefix, moniker) of the site [Unicode], or None"),
+    ('html_pagetitle', None, "Allows you to set a specific HTML page title (if None, it defaults to the value of `sitename`) [Unicode]"),
+    ('navi_bar', [u'FindPage', u'HelpContents', ],
+     'Most important page names. Users can add more names in their quick links in user preferences. To link to URL, use `u"[[url|link title]]"`, to use a shortened name for long page name, use `u"[[LongLongPageName|title]]"`. [list of Unicode]'),
+
+    ('theme_default', u'modernized', "Default theme."),
+
+    ('serve_files', {},
+     """
+     Dictionary of name: filesystem_path for static file resources to serve
+     from the filesystem as url .../+serve/<name>/...
+     """),
+
+    ('supplementation_item_names', [u'Discussion', ],
+     "List of names of the supplementation (sub)items [Unicode]"),
+
+    ('interwiki_preferred', [], "In dialogues, show those wikis at the top of the list [list of Unicode]."),
+    ('sistersites', [], "list of tuples `('WikiName', 'sisterpagelist_fetch_url')`"),
+
+    ('trail_size', 5,
+     "Number of items in the trail of recently visited items"),
+
+    ('edit_bar', ['Show', 'Highlight', 'Meta', 'Modify', 'Comments', 'Download', 'History', 'Subscribe', 'Quicklink', 'Index', 'Supplementation', 'ActionsMenu'],
+     'list of edit bar entries'),
+    ('history_count', (100, 200), "number of revisions shown for info/history action (default_count_shown, max_count_shown)"),
+
+    ('show_hosts', True,
+     "if True, show host names and IPs. Set to False to hide them."),
+    ('show_interwiki', False,
+     "if True, let the theme display your interwiki name"),
+    ('show_names', True,
+     "if True, show user names in the revision history and on Recent``Changes. Set to False to hide them."),
+    ('show_section_numbers', False,
+     'show section numbers in headings by default'),
+    ('show_rename_redirect', False, "if True, offer creation of redirect pages when renaming wiki pages"),
+
+    ('template_dirs', [], "list of directories with templates that will override theme and base templates."),
+  )),
+  # ==========================================================================
+  'editor': ('Editor related', None, (
+    ('item_license', u'', 'if set, show the license item within the editor. [Unicode]'),
+    ('edit_locking', 'warn 10', "Editor locking policy: `None`, `'warn <timeout in minutes>'`, or `'lock <timeout in minutes>'`"),
+    ('edit_ticketing', True, None),
+  )),
+  # ==========================================================================
+  'data': ('Data storage', None, (
+    ('data_dir', './data/', "Path to the data directory."),
+    ('plugin_dir', None, "Plugin directory, by default computed to be `data_dir`/plugin."),
+    ('plugin_dirs', [], "Additional plugin directories."),
+
+    ('interwiki_map', {},
+     "Dictionary of wiki_name -> wiki_url"),
+    ('namespace_mapping', None,
+    "This needs to point to a (correctly ordered!) list of tuples, each tuple containing: Namespace identifier, backend, acl protection to be applied to that backend. " + \
+    "E.g.: [('/', FSBackend('wiki/data'), dict(default='All:read,write,create')), ]. Please see HelpOnStorageConfiguration for further reference."),
+    ('index_rebuild', True,
+     'rebuild item index from scratch (you may set this to False to speedup startup once you have an index)'),
+    ('load_xml', None,
+     'If this points to an xml file, the file is loaded into the storage backend(s) upon first request.'),
+    ('save_xml', None,
+     'If this points to an xml file, the current storage backend(s) content is saved into that file upon the first request.'),
+  )),
+  # ==========================================================================
+  'items': ('Special item names', None, (
+    ('item_root', u'Home', "Name of the root item (aka 'front page'). [Unicode]"),
+
+    # the following regexes should match the complete name when used in free text
+    # the group 'all' shall match all, while the group 'key' shall match the key only
+    # e.g. FooGroup -> group 'all' ==  FooGroup, group 'key' == Foo
+    # moin's code will add ^ / $ at beginning / end when needed
+    ('item_dict_regex', ur'(?P<all>(?P<key>\S+)Dict)',
+     'Item names exactly matching this regex are regarded as items containing variable dictionary definitions [Unicode]'),
+    ('item_group_regex', ur'(?P<all>(?P<key>\S+)Group)',
+     'Item names exactly matching this regex are regarded as items containing group definitions [Unicode]'),
+  )),
+  # ==========================================================================
+  'user': ('User Preferences related', None, (
+    ('user_defaults',
+      dict(
+        name=u'anonymous',
+        aliasname=None,
+        email=None,
+        openid=None,
+        css_url=None,
+        mailto_author=False,
+        edit_on_doubleclick=True,
+        show_comments=False,
+        want_trivial=False,
+        disabled=False,
+        quicklinks=[],
+        subscribed_items=[],
+        email_subscribed_events=[
+            # XXX PageChangedEvent.__name__
+            # XXX PageRenamedEvent.__name__
+            # XXX PageDeletedEvent.__name__
+            # XXX PageCopiedEvent.__name__
+            # XXX PageRevertedEvent.__name__
+        ],
+        theme_name=None, # None -> use cfg.theme_default
+        edit_rows=0,
+        locale=None, # None -> do browser language detection, otherwise just use this locale
+        timezone=None, # None -> use cfg.timezone_default
+      ),
+     'Default attributes of the user object'),
+  )),
+  # ==========================================================================
+  'various': ('Various', None, (
+    ('bang_meta', True, 'if True, enable {{{!NoWikiName}}} markup'),
+
+    ('config_check_enabled', False, "if True, check configuration for unknown settings."),
+
+    ('timezone_default', u'UTC', "Default time zone."),
+    ('locale_default', u'en_US', "Default locale for user interface and content."),
+
+    ('log_remote_addr', True,
+     "if True, log the remote IP address (and maybe hostname)."),
+    ('log_reverse_dns_lookups', True,
+     "if True, do a reverse DNS lookup on page SAVE. If your DNS is broken, set this to False to speed up SAVE."),
+
+    # some dangerous mimetypes (we don't use "content-disposition: inline" for them when a user
+    # downloads such data, because the browser might execute e.g. Javascript contained
+    # in the HTML and steal your moin session cookie or do other nasty stuff)
+    ('mimetypes_xss_protect',
+     [
+       'text/html',
+       'application/x-shockwave-flash',
+       'application/xhtml+xml',
+     ],
+     '"content-disposition: inline" is not used for downloads of such data'),
+
+    ('mimetypes_embed',
+     [
+       'application/x-dvi',
+       'application/postscript',
+       'application/pdf',
+       'application/ogg',
+       'application/vnd.visio',
+       'image/x-ms-bmp',
+       'image/svg+xml',
+       'image/tiff',
+       'image/x-photoshop',
+       'audio/mpeg',
+       'audio/midi',
+       'audio/x-wav',
+       'video/fli',
+       'video/mpeg',
+       'video/quicktime',
+       'video/x-msvideo',
+       'chemical/x-pdb',
+       'x-world/x-vrml',
+     ],
+     'mimetypes that can be embedded by the [[HelpOnMacros/EmbedObject|EmbedObject macro]]'),
+
+    ('refresh', None,
+     "refresh = (minimum_delay_s, targets_allowed) enables use of `#refresh 5 PageName` processing instruction, targets_allowed must be either `'internal'` or `'external'`"),
+
+    ('search_results_per_page', 25, "Number of hits shown per page in the search results"),
+
+    ('siteid', 'MoinMoin', None), # XXX just default to some existing module name to
+                                  # make plugin loader etc. work for now
+  )),
+}
+
+#
+# The 'options' dict carries default MoinMoin options. The dict is a
+# group name to tuple mapping.
+# Each group tuple consists of the following items:
+#   group section heading, group help text, option list
+#
+# where each 'option list' is a tuple or list of option tuples
+#
+# each option tuple consists of
+#   option name, default value, help text
+#
+# All the help texts will be displayed by the WikiConfigHelp() macro.
+#
+# Unlike the options_no_group_name dict, option names in this dict
+# are automatically prefixed with "group name '_'" (i.e. the name of
+# the group they are in and an underscore), e.g. the 'hierarchic'
+# below creates an option called "acl_hierarchic".
+#
+# If you need to add a complex default expression that results in an
+# object and should not be shown in the __repr__ form in WikiConfigHelp(),
+# you can use the DefaultExpression class, see 'auth' above for example.
+#
+#
+options = {
+    'acl': ('Access control lists',
+    'ACLs control who may do what, see HelpOnAccessControlLists.',
+    (
+      ('rights_valid', config.ACL_RIGHTS_VALID,
+       "Valid tokens for right sides of ACL entries."),
+    )),
+
+    'ns': ('Storage Namespaces',
+    "Storage namespaces can be defined for all sorts of data. All items sharing a common namespace as prefix" + \
+    "are then stored within the same backend. The common prefix for all data is ''.",
+    (
+      ('content', '/', "All content is by default stored below /, hence the prefix is ''."),  # Not really necessary. Just for completeness.
+      ('user_profile', 'UserProfile/', 'User profiles (i.e. user data, not their homepage) are stored in this namespace.'),
+      ('user_homepage', 'User/', 'All user homepages are stored below this namespace.'),
+      ('trash', 'Trash/', 'This is the namespace in which an item ends up when it is deleted.')
+    )),
+
+    'xapian': ('Xapian search', "Configuration of the Xapian based indexed search, see HelpOnXapian.", (
+      ('search', False,
+       "True to enable the fast, indexed search (based on the Xapian search library)"),
+      ('index_dir', None,
+       "Directory where the Xapian search index is stored (None = auto-configure wiki local storage)"),
+      ('stemming', False,
+       "True to enable Xapian word stemmer usage for indexing / searching."),
+      ('index_history', False,
+       "True to enable indexing of non-current page revisions."),
+    )),
+
+    'user': ('Users / User settings', None, (
+      ('email_unique', True,
+       "if True, check email addresses for uniqueness and don't accept duplicates."),
+
+      ('homewiki', u'Self',
+       "interwiki name of the wiki where the user home pages are located [Unicode] - useful if you have ''many'' users. You could even link to nonwiki \"user pages\" if the wiki username is in the target URL."),
+    )),
+
+    'mail': ('Mail settings',
+        'These settings control outgoing and incoming email from and to the wiki.',
+    (
+      ('from', None, "Used as From: address for generated mail. [Unicode]"),
+      ('login', None, "'username userpass' for SMTP server authentication (None = don't use auth)."),
+      ('smarthost', None, "Address of SMTP server to use for sending mail (None = don't use SMTP server)."),
+      ('sendmail', None, "sendmail command to use for sending mail (None = don't use sendmail)"),
+    )),
+}
+
+def _add_options_to_defconfig(opts, addgroup=True):
+    for groupname in opts:
+        group_short, group_doc, group_opts = opts[groupname]
+        for name, default, doc in group_opts:
+            if addgroup:
+                name = groupname + '_' + name
+            if isinstance(default, DefaultExpression):
+                default = default.value
+            setattr(DefaultConfig, name, default)
+
+_add_options_to_defconfig(options)
+_add_options_to_defconfig(options_no_group_name, False)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/conftest.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,148 @@
+# -*- coding: iso-8859-1 -*-
+"""
+MoinMoin Testing Framework
+--------------------------
+
+All test modules must be named test_modulename to be included in the
+test suite. If you are testing a package, name the test module
+test_package_module.
+
+Tests that require a certain configuration, like section_numbers = 1, must
+use a Config class to define the required configuration within the test class.
+
+@copyright: 2005 MoinMoin:NirSoffer,
+            2007 MoinMoin:AlexanderSchremmer,
+            2008 MoinMoin:ThomasWaldmann
+@license: GNU GPL, see COPYING for details.
+"""
+
+# exclude some directories from py.test test discovery, pathes relative to this file
+collect_ignore = ['static',  # same
+                  '../wiki', # no tests there
+                  '../instance', # tw likes to use this for wiki data (non-revisioned)
+                 ]
+
+import atexit
+import os
+import sys
+
+import py
+
+from MoinMoin.app import create_app_ext, before_wiki, after_wiki
+from MoinMoin._tests import maketestwiki, wikiconfig
+from MoinMoin.storage.backends import create_simple_mapping
+
+coverage_modules = set()
+
+try:
+    """
+    This code adds support for coverage.py (see
+    http://nedbatchelder.com/code/modules/coverage.html).
+    It prints a coverage report for the modules specified in all
+    module globals (of the test modules) named "coverage_modules".
+    """
+
+    import coverage
+
+    def report_coverage():
+        coverage.stop()
+        module_list = [sys.modules[mod] for mod in coverage_modules]
+        module_list.sort()
+        coverage.report(module_list)
+
+    def callback(option, opt_str, value, parser):
+        atexit.register(report_coverage)
+        coverage.erase()
+        coverage.start()
+
+    py.test.config.addoptions('MoinMoin options', py.test.config.Option('-C',
+        '--coverage', action='callback', callback=callback,
+        help='Output information about code coverage (slow!)'))
+
+except ImportError:
+    coverage = None
+
+
+def init_test_app(given_config):
+    namespace_mapping, router_index_uri = create_simple_mapping("memory:", given_config.content_acl)
+    more_config = dict(
+        namespace_mapping=namespace_mapping,
+        router_index_uri=router_index_uri,
+    )
+    app = create_app_ext(flask_config_dict=dict(SECRET_KEY='foobarfoobar'),
+                         moin_config_class=given_config,
+                         **more_config)
+    ctx = app.test_request_context('/')
+    ctx.push()
+    before_wiki()
+    return app, ctx
+
+def deinit_test_app(ctx):
+    after_wiki('')
+    ctx.pop()
+
+
+class MoinClassCollector(py.test.collect.Class):
+
+    def setup(self):
+        cls = self.obj
+        if hasattr(cls, 'Config'):
+            given_config = cls.Config
+        else:
+            given_config = wikiconfig.Config
+        cls.app, cls.ctx = init_test_app(given_config)
+
+        def setup_method(f):
+            def wrapper(self, *args, **kwargs):
+                self.app, self.ctx = init_test_app(given_config)
+                # Don't forget to call the class' setup_method if it has one.
+                return f(self, *args, **kwargs)
+            return wrapper
+
+        def teardown_method(f):
+            def wrapper(self, *args, **kwargs):
+                deinit_test_app(self.ctx)
+                # Don't forget to call the class' teardown_method if it has one.
+                return f(self, *args, **kwargs)
+            return wrapper
+
+        try:
+            # Wrap the actual setup_method in our decorator.
+            cls.setup_method = setup_method(cls.setup_method)
+        except AttributeError:
+            # Perhaps the test class did not define a setup_method.
+            def no_setup(self, method):
+                self.app, self.ctx = init_test_app(given_config)
+            cls.setup_method = no_setup
+
+        try:
+            # Wrap the actual teardown_method in our decorator.
+            cls.teardown_method = setup_method(cls.teardown_method)
+        except AttributeError:
+            # Perhaps the test class did not define a teardown_method.
+            def no_teardown(self, method):
+                deinit_test_app(self.ctx)
+            cls.teardown_method = no_teardown
+
+        super(MoinClassCollector, self).setup()
+
+    def teardown(self):
+        cls = self.obj
+        deinit_test_app(cls.ctx)
+        super(MoinClassCollector, self).teardown()
+
+
+class Module(py.test.collect.Module):
+    Class = MoinClassCollector
+
+    def __init__(self, *args, **kwargs):
+        given_config = wikiconfig.Config
+        self.app, self.ctx = init_test_app(given_config)
+        # XXX do ctx.pop() in ... (where?)
+        super(Module, self).__init__(*args, **kwargs)
+
+    def run(self, *args, **kwargs):
+        if coverage is not None:
+            coverage_modules.update(getattr(self.obj, 'coverage_modules', []))
+        return super(Module, self).run(*args, **kwargs)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/converter/__init__.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,106 @@
+"""
+MoinMoin - Converter support
+
+Converters are used to convert between formats or between different featuresets
+of one format.
+
+There are usually three types of converters:
+- Between an input format like Moin Wiki or Creole and the internal tree
+  representation.
+- Between the internal tree and an output format like HTML.
+- Between different featuresets of the internal tree representation like URI
+  types or macro expansion.
+
+TODO: Merge with new-style macros.
+
+@copyright: 2008 MoinMoin:BastianBlank
+@license: GNU GPL, see COPYING for details.
+"""
+
+from ..util.registry import RegistryBase
+
+
+class RegistryConverter(RegistryBase):
+    class Entry(object):
+        def __init__(self, factory, type_input, type_output, priority):
+            self.factory = factory
+            self.type_input = type_input
+            self.type_output = type_output
+            self.priority = priority
+
+        def __call__(self, type_input, type_output, kw):
+            if (self.type_output.issupertype(type_output) and
+                    self.type_input.issupertype(type_input)):
+                    return self.factory(type_input, type_output, **kw)
+
+        def __eq__(self, other):
+            if isinstance(other, self.__class__):
+                return (self.factory == other.factory and
+                        self.type_input == other.type_input and
+                        self.type_output == other.type_output and
+                        self.priority == other.priority)
+            return NotImplemented
+
+        def __lt__(self, other):
+            if isinstance(other, self.__class__):
+                if self.priority < other.priority:
+                    return True
+                if self.type_output != other.type_output:
+                    if other.type_output.issupertype(self.type_output):
+                        return True
+                    return False
+                if self.type_input != other.type_input:
+                    if other.type_input.issupertype(self.type_input):
+                        return True
+                    return False
+                return False
+            return NotImplemented
+
+        def __repr__(self):
+            return '<%s: input %s, output %s, prio %d [%r]>' % (self.__class__.__name__,
+                    self.type_input,
+                    self.type_output,
+                    self.priority,
+                    self.factory)
+
+    def get(self, type_input, type_output, **kw):
+        for entry in self._entries:
+            conv = entry(type_input, type_output, kw)
+            if conv is not None:
+                return conv
+
+    def register(self, factory, type_input, type_output, priority=RegistryBase.PRIORITY_MIDDLE):
+        """
+        Register a factory
+
+        @param factory: Factory to register. Callable, must return an object
+        """
+        return self._register(self.Entry(factory, type_input, type_output, priority))
+
+
+# TODO: Move somewhere else. Also how to do that for per-wiki modules?
+def _load():
+    import imp, os, sys
+    for path in __path__:
+        for root, dirs, files in os.walk(path):
+            del dirs[:]
+            for file in files:
+                if file.startswith('_') or not file.endswith('.py'):
+                    continue
+                module = file[:-3]
+                module_complete = __name__ + '.' + module
+                if module_complete in sys.modules:
+                    continue
+                info = imp.find_module(module, [root])
+                try:
+                    try:
+                        imp.load_module(module_complete, *info)
+                    except Exception, e:
+                        import MoinMoin.log as logging
+                        logger = logging.getLogger(__name__)
+                        logger.exception("Failed to import converter package %s: %s" % (module, e))
+                finally:
+                    info[0].close()
+
+default_registry = RegistryConverter()
+_load()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/converter/_args.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,68 @@
+"""
+MoinMoin - Arguments wrapper
+
+@copyright: 2009 MoinMoin:BastianBlank
+@license: GNU GPL, see COPYING for details.
+"""
+
+
+class Arguments(object):
+    """
+    Represent an argument list that may contain positional or keyword args.
+    """
+    __slots__ = 'positional', 'keyword'
+
+    def __init__(self, positional=None, keyword=None):
+        self.positional = positional and positional[:] or []
+        self.keyword = keyword and keyword.copy() or {}
+
+    def __contains__(self, key):
+        """
+        Check for positional argument or keyword key presence.
+        """
+        return key in self.positional or key in self.keyword
+
+    def __getitem__(self, key):
+        """
+        Access positional arguments by index or keyword args by key name.
+        """
+        if isinstance(key, (int, slice)):
+            return self.positional[key]
+        return self.keyword[key]
+
+    def __len__(self):
+        """
+        Total count of positional + keyword args.
+        """
+        return len(self.positional) + len(self.keyword)
+
+    def __repr__(self):
+        return '<%s(%r, %r)>' % (self.__class__.__name__,
+                self.positional, self.keyword)
+
+    def items(self):
+        """
+        Return an iterator over all (key, value) pairs.
+        Positional arguments are assumed to have a None key.
+        """
+        for value in self.positional:
+            yield None, value
+        for item in self.keyword.iteritems():
+            yield item
+
+    def keys(self):
+        """
+        Return an iterator over all keys from the keyword arguments.
+        """
+        for key in self.keyword.iterkeys():
+            yield key
+
+    def values(self):
+        """
+        Return an iterator over all values.
+        """
+        for value in self.positional:
+            yield value
+        for value in self.keyword.itervalues():
+            yield value
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/converter/_args_wiki.py	Sun Feb 20 20:53:45 2011 +0100
@@ -0,0 +1,85 @@
+"""
+MoinMoin - Arguments support for wiki formats
+
+@copyright: 2009 MoinMoin:BastianBlank
+@license: GNU GPL, see COPYING for details.
+"""
+
+from __future__ import absolute_import
+
+import re
+
+from ._args import Arguments
+
+# see parse() docstring for example
+_parse_rules = r'''
+(?:
+    ([-\w]+)
+    =
+)?
+(?:
+    ([-\w]+)
+    |
+    "
+    (.*?)
+    (?<!\\)"
+    |
+    '
+    (.*?)
+    (?<!\\)'
+)
+'''
+_parse_re = re.compile(_parse_rules, re.X)
+
+def parse(input):
+    """